diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ad48fa5..41b637ab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,12 +17,7 @@ updates: directory: "/crates/code2prompt-core" schedule: interval: "weekly" - - package-ecosystem: "pip" # pyproject.toml - target-branch: "main" - directory: "/crates/code2prompt-python" - schedule: - interval: "weekly" - - package-ecosystem: "uv" # requirements.lock + - package-ecosystem: "uv" # pyproject.toml and uv.lock target-branch: "main" directory: "/crates/code2prompt-python" schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70c79a5d..f30a7443 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,29 @@ jobs: - uses: actions/checkout@v6 - name: Run tests run: cargo test --verbose + + python-bindings: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.14"] + + steps: + - uses: actions/checkout@v6 + - name: Install uv and Python + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.10" + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: crates/code2prompt-python/uv.lock + - name: Sync Python project + working-directory: crates/code2prompt-python + run: uv sync --locked + - name: Run Python tests + working-directory: crates/code2prompt-python + run: uv run --locked pytest + - name: Build release wheel + if: matrix.python-version == '3.11' + working-directory: crates/code2prompt-python + run: uv run --locked maturin build --release --out ../../dist diff --git a/Cargo.lock b/Cargo.lock index 6285adc7..d69f6d9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,11 +411,10 @@ dependencies = [ [[package]] name = "code2prompt-python" -version = "3.2.0" +version = "4.3.0" dependencies = [ "code2prompt_core", "pyo3", - "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7ddf24c0..71f4e866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ git2 = { version = "0.21.0", default-features = false, features = [ globset = "0.4.15" handlebars = "6.4.0" once_cell = "1.19.0" -pyo3 = { version = "0.28.3", features = ["extension-module", "abi3-py312"] } +pyo3 = { version = "0.28.3", features = ["extension-module", "abi3-py311"] } ratatui = "0.30.1" regex = "1.10.3" rayon = "1.11.0" diff --git a/crates/code2prompt-python/Cargo.toml b/crates/code2prompt-python/Cargo.toml index 0b09cc2a..9892a232 100644 --- a/crates/code2prompt-python/Cargo.toml +++ b/crates/code2prompt-python/Cargo.toml @@ -1,13 +1,22 @@ [package] name = "code2prompt-python" -version = "3.2.0" +version = "4.3.0" +authors = [ + "Olivier D'Ancona ", + "Mufeed VH ", +] +description = "Native Python bindings for code2prompt" +homepage = "https://code2prompt.dev" +documentation = "https://code2prompt.dev/docs/welcome" +repository = "https://github.com/mufeedvh/code2prompt" +license = "MIT" edition = "2024" +readme = "README.md" [lib] name = "code2prompt_rs" crate-type = ["cdylib"] [dependencies] -serde_json = { workspace = true } code2prompt_core = { path = "../code2prompt-core" } pyo3 = { workspace = true } diff --git a/crates/code2prompt-python/README.md b/crates/code2prompt-python/README.md new file mode 100644 index 00000000..48826a7f --- /dev/null +++ b/crates/code2prompt-python/README.md @@ -0,0 +1,115 @@ +# code2prompt Python bindings + +Native Python bindings for the stateful +[`code2prompt_core`](https://crates.io/crates/code2prompt_core) API. + +## Installation + +```bash +pip install code2prompt_rs +``` + +Python 3.11 or newer is supported. + +Rust `PathBuf` values accept Python strings or `os.PathLike` objects and are exposed as +`pathlib.Path` objects. + +## Usage + +Create a core configuration, move it into a session, and operate on that session just as you +would from Rust: + +```python +from code2prompt_rs import Code2PromptConfig, Code2PromptSession + +config = Code2PromptConfig( + ".", + include_patterns=["**/*.py", "**/*.rs"], + exclude_patterns=["**/tests/**"], + line_numbers=True, + deselected=True, +) +session = Code2PromptSession(config) + +session.select_file("src/main.rs") +session.select_file("src/lib.rs") + +result = session.generate_prompt() +print(result.prompt) +print(result.token_count) +print(result.files) +``` + +Selection methods mutate and return the same session, so calls may also be chained: + +```python +session.select_file("src/main.rs").deselect_file("src/generated.rs") +``` + +## Configuration + +Configuration values use native enum classes rather than string aliases: + +```python +from code2prompt_rs import ( + Code2PromptConfig, + FileProcessorsConfig, + FileSortMethod, + IpynbProcessorConfig, + OutputFormat, + TokenFormat, + TokenizerType, +) + +config = Code2PromptConfig( + ".", + absolute_path=False, + no_codeblock=False, + output_format=OutputFormat.Markdown, + sort_method=FileSortMethod.NameAsc, + encoding=TokenizerType.Cl100kBase, + token_format=TokenFormat.Raw, + processors=FileProcessorsConfig( + ipynb=IpynbProcessorConfig( + max_code_cells=5, + include_outputs=True, + include_markdown=True, + ) + ), +) +``` + +`Code2PromptConfig` exposes the user-facing fields from core 4.3. The optional Rust +`entity-map` feature is not included in the Python wheels. + +## Session API + +`Code2PromptSession` exposes the core operational methods: + +- Pattern updates: `add_include_pattern`, `add_exclude_pattern` +- Selection: `select_file`, `deselect_file`, `toggle_file_selection`, + `is_file_selected`, `get_selected_files`, `clear_user_actions`, `has_user_actions`, + `set_deselected` +- Loading: `load_codebase`, `load_git_diff`, `load_git_diff_between_branches`, + `load_git_log_between_branches` +- Results: `generate_prompt`, `raw_analysis`, `contextual_analysis` + +`generate_prompt()` returns a typed `RenderedPrompt`. Loaded codebase and Git values are +available from the typed `session.data` snapshot. Analysis calls return `CodebaseAnalysis`, +whose `raw_files()`, `by_extension()`, and `token_map(options)` methods return typed models. + +## Local development + +```bash +uv sync +uv run pytest +``` + +`uv sync` creates `.venv`, installs the development dependencies, and builds the native +extension in editable mode. The lockfile is committed for reproducible development environments. + +To build a release wheel locally: + +```bash +uv run maturin build --release +``` diff --git a/crates/code2prompt-python/examples/basic_usage.py b/crates/code2prompt-python/examples/basic_usage.py new file mode 100644 index 00000000..497c67f1 --- /dev/null +++ b/crates/code2prompt-python/examples/basic_usage.py @@ -0,0 +1,23 @@ +"""Select files and generate a prompt through the native session API.""" + +from code2prompt_rs import Code2PromptConfig, Code2PromptSession + + +def main() -> None: + config = Code2PromptConfig( + ".", + include_patterns=["**/*.py", "**/*.rs"], + exclude_patterns=["**/tests/**"], + line_numbers=True, + deselected=True, + ) + session = Code2PromptSession(config) + session.select_file("src/main.rs").select_file("src/lib.rs") + + result = session.generate_prompt() + print(f"Generated {result.token_count} tokens from {len(result.files)} files") + print(result.prompt) + + +if __name__ == "__main__": + main() diff --git a/crates/code2prompt-python/pyproject.toml b/crates/code2prompt-python/pyproject.toml index b39d6416..7917d2cd 100644 --- a/crates/code2prompt-python/pyproject.toml +++ b/crates/code2prompt-python/pyproject.toml @@ -1,12 +1,13 @@ [project] name = "code2prompt_rs" -version = "3.2.1" +version = "4.3.0" description = "Python bindings for code2prompt" authors = [ { name = "Olivier D'Ancona", email = "olivier_dancona@hotmail.com" }, { name = "Mufeed VH", email = "contact@mufeedvh.com" }, ] -dependencies = ["pip>=25.0.1", "patchelf>=0.17.2.1"] +dependencies = [] +readme = "README.md" requires-python = ">= 3.11" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -16,33 +17,39 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Rust", "Topic :: Software Development :: Libraries :: Python Modules", ] [build-system] -requires = ["maturin>=1.0,<2.0"] +requires = ["maturin>=1.8.2,<2.0"] build-backend = "maturin" [tool.maturin] bindings = "pyo3" module-name = "code2prompt_rs" manifest-path = "Cargo.toml" -python-source = "python-sdk" features = ["pyo3/extension-module"] -[tool.rye] -managed = true -dev-dependencies = ["maturin>=1.8.2", "pytest>=8.3.5"] - -[tool.rye.scripts] -build = "maturin develop" - -[tool.hatch.metadata] -allow-direct-references = true +[dependency-groups] +dev = [ + "maturin>=1.8.2,<2.0", + "pytest>=8.3.5", +] -[tool.hatch.build.targets.wheel] -packages = ["src/python_sdk"] +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "Cargo.toml" }, + { file = "../../Cargo.toml" }, + { file = "../../Cargo.lock" }, + { file = "src/**/*.rs" }, + { file = "../code2prompt-core/Cargo.toml" }, + { file = "../code2prompt-core/src/**/*.rs" }, + { file = "../code2prompt-core/templates/**/*" }, +] [project.urls] Homepage = "https://code2prompt.dev" diff --git a/crates/code2prompt-python/python-sdk/.gitignore b/crates/code2prompt-python/python-sdk/.gitignore deleted file mode 100644 index 7dbdfee6..00000000 --- a/crates/code2prompt-python/python-sdk/.gitignore +++ /dev/null @@ -1,171 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# PyPI configuration file -.pypirc \ No newline at end of file diff --git a/crates/code2prompt-python/python-sdk/README.md b/crates/code2prompt-python/python-sdk/README.md deleted file mode 100644 index 0db3b03a..00000000 --- a/crates/code2prompt-python/python-sdk/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# code2prompt Python SDK - -Python bindings for [code2prompt](https://github.com/mufeedvh/code2prompt) - A tool to generate LLM prompts from codebases. - -## Installation - -### Local Development Installation - -1. Clone the repository: - -```bash -git clone https://github.com/mufeedvh/code2prompt.git -cd code2prompt -``` - -2. Install development dependencies: - -```bash -python3 -m venv .venv -source .venv/bin/activate -pip install maturin pytest -``` - -3. Build and install the package locally: - -```bash -cd code2prompt/ # root repo directory -maturin develop -r -``` - -### Running Examples - -Try out the example script: - -```bash -python examples/basic_usage.py -``` - -## Usage - -```python -from code2prompt import CodePrompt - -# Create a new CodePrompt instance -prompt = CodePrompt( - path="./my_project", - include_patterns=["*.py", "*.rs"], # Optional: Only include Python and Rust files - exclude_patterns=["**/tests/*"], # Optional: Exclude test files - line_numbers=True, # Optional: Add line numbers to code -) - -# Generate a prompt -result = prompt.generate( - template=None, # Optional: Custom Handlebars template - encoding="cl100k" # Optional: Token encoding (for token counting) -) - -# Access the generated prompt and metadata -print(f"Generated prompt: {result['prompt']}") -print(f"Token count: {result['token_count']}") -print(f"Model info: {result['model_info']}") - -# Git operations -git_diff = prompt.get_git_diff() -branch_diff = prompt.get_git_diff_between_branches("main", "feature") -git_log = prompt.get_git_log("main", "feature") -``` - -## API Reference - -### `CodePrompt` - -Main class for generating prompts from code. - -#### Constructor - -```python -CodePrompt( - path: str, - include_patterns: List[str] = [], - exclude_patterns: List[str] = [], - include_priority: bool = False, - line_numbers: bool = False, - relative_paths: bool = False, - exclude_from_tree: bool = False, - no_codeblock: bool = False, - follow_symlinks: bool = False -) -``` - -- `path`: Path to the codebase directory -- `include_patterns`: List of glob patterns for files to include -- `exclude_patterns`: List of glob patterns for files to exclude -- `include_priority`: Give priority to include patterns in case of conflicts -- `line_numbers`: Add line numbers to code blocks -- `relative_paths`: Use relative paths instead of absolute -- `exclude_from_tree`: Exclude files from source tree based on patterns -- `no_codeblock`: Don't wrap code in markdown code blocks -- `follow_symlinks`: Follow symbolic links when traversing directories - -#### Methods - -##### `generate(template: Optional[str] = None, encoding: Optional[str] = None) -> Dict` - -Generate a prompt from the codebase. - -- `template`: Optional custom Handlebars template -- `encoding`: Optional token encoding (cl100k, p50k, p50k_edit, r50k, gpt2) - -Returns a dictionary containing: - -- `prompt`: The generated prompt -- `directory`: The processed directory path -- `token_count`: Number of tokens (if encoding was specified) -- `model_info`: Information about the model (if encoding was specified) - -##### `get_git_diff() -> str` - -Get git diff for the repository. - -##### `get_git_diff_between_branches(branch1: str, branch2: str) -> str` - -Get git diff between two branches. - -##### `get_git_log(branch1: str, branch2: str) -> str` - -Get git log between two branches. - -## License - -MIT License - see LICENSE file for details. diff --git a/crates/code2prompt-python/python-sdk/__init__.py b/crates/code2prompt-python/python-sdk/__init__.py deleted file mode 100644 index 43c07114..00000000 --- a/crates/code2prompt-python/python-sdk/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -code2prompt is a Python library for generating LLM prompts from codebases. - -It provides a simple interface to the Rust-based code2prompt library, allowing you to: -- Generate prompts from code directories -- Filter files using glob patterns -- Get git diffs and logs -- Count tokens for different models -""" - -# Import the Python wrapper class from the renamed file -from .code2prompt_rs import Code2Prompt - -__all__ = ['Code2Prompt'] \ No newline at end of file diff --git a/crates/code2prompt-python/python-sdk/code2prompt_rs/__init__.py b/crates/code2prompt-python/python-sdk/code2prompt_rs/__init__.py deleted file mode 100644 index b0bfa865..00000000 --- a/crates/code2prompt-python/python-sdk/code2prompt_rs/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -code2prompt is a Python library for generating LLM prompts from codebases. - -It provides a simple interface to the Rust-based code2prompt library, allowing you to: -- Generate prompts from code directories -- Filter files using glob patterns -- Get git diffs and logs -- Count tokens for different models -""" - -# Import the Python wrapper class from the renamed file -from .code2prompt import Code2Prompt - -__all__ = ['Code2Prompt'] \ No newline at end of file diff --git a/crates/code2prompt-python/python-sdk/code2prompt_rs/code2prompt.py b/crates/code2prompt-python/python-sdk/code2prompt_rs/code2prompt.py deleted file mode 100644 index a374fb17..00000000 --- a/crates/code2prompt-python/python-sdk/code2prompt_rs/code2prompt.py +++ /dev/null @@ -1,116 +0,0 @@ -# Import the Rust module -from . import code2prompt_rs as rust_sdk -from pathlib import Path - -class RenderedPrompt: - def __init__(self, prompt, token_count, directory, model_info): - self.prompt = prompt - self.token_count = token_count - self.directory = directory - self.model_info = model_info - -class Code2Prompt: - def __init__(self, path, include_patterns=None, exclude_patterns=None, - include_priority=False, line_numbers=False, absolute_paths=False, - full_directory_tree=False, code_blocks=True, follow_symlinks=False, include_hidden=False): - """ - Initialize a Code2Prompt configuration for generating prompts from code. - - Args: - path: Path to the code directory - include_patterns: List of glob patterns for files to include - exclude_patterns: List of glob patterns for files to exclude - include_priority: Whether to prioritize include patterns over exclude - line_numbers: Whether to include line numbers in the output - absolute_paths: Whether to use absolute paths in the output - full_directory_tree: Whether to include the full directory tree - code_blocks: Whether to wrap code in markdown code blocks - follow_symlinks: Whether to follow symlinks - include_hidden: Whether to include hidden files (default is False) - """ - # Stocker la configuration - self.path = Path(path) - self.include_patterns = include_patterns or [] - self.exclude_patterns = exclude_patterns or [] - self.include_priority = include_priority - self.line_numbers = line_numbers - self.absolute_paths = absolute_paths - self.full_directory_tree = full_directory_tree - self.code_blocks = code_blocks - self.follow_symlinks = follow_symlinks - self.include_hidden = include_hidden - - # Initializer une session uniquement quand nécessaire - self._session = None - - def session(self) -> rust_sdk.PyCode2PromptSession: - """ - Create a PyCode2PromptSession with the current configuration. - """ - # Créer la session Rust avec la configuration actuelle - session = rust_sdk.PyCode2PromptSession(str(self.path)) - - # Appliquer toutes les configurations - if self.include_patterns: - session = session.include(self.include_patterns) - if self.exclude_patterns: - session = session.exclude(self.exclude_patterns) - - session = session.include_priority(self.include_priority) - session = session.with_line_numbers(self.line_numbers) - session = session.with_absolute_paths(self.absolute_paths) - session = session.with_full_directory_tree(self.full_directory_tree) - session = session.with_code_blocks(self.code_blocks) - session = session.follow_symlinks(self.follow_symlinks) - session = session.include_hidden(self.include_hidden) - - return session - - def generate(self, template=None, encoding=None) -> RenderedPrompt: - """ - Generate a prompt from the code. - - Args: - template: Optional template string to use - encoding: Token encoding to use (e.g., 'cl100k', 'gpt2') - - Returns: - String containing the generated prompt - """ - # Apply optional configurations - session = self._session or self.session() - - if encoding: - session = session.with_token_encoding(encoding) - - if template: - session = session.with_template(template) - - # Generate the prompt - result = session.generate() - - # Get token count - try: - token_count = session.token_count() - except Exception: - token_count = 0 - - # Return a dictionary with results - return RenderedPrompt( - prompt=result, - token_count=token_count, - directory=self.path, - model_info=session.info() - ) - - def token_count(self, encoding=None): - """Get token count for the prompt with specified encoding.""" - session = self._session or self.session() - if encoding: - session = session.with_token_encoding(encoding) - return session.token_count() - - def info(self): - """Get information about the current session.""" - session = self._session or self.session() - return session.info() \ No newline at end of file diff --git a/crates/code2prompt-python/python-sdk/examples/basic_usage.py b/crates/code2prompt-python/python-sdk/examples/basic_usage.py deleted file mode 100644 index d502c60e..00000000 --- a/crates/code2prompt-python/python-sdk/examples/basic_usage.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Example usage of the code2prompt Python SDK.""" - -from code2prompt_rs import Code2Prompt - -def main(): - # Create a Code2Prompt instance for the current directory - prompt = Code2Prompt( - path=".", - include_patterns=["*.py", "*.rs"], # Only include Python and Rust files - exclude_patterns=["**/tests/*"], # Exclude test files - line_numbers=True # Add line numbers to code - ) - - # Generate a prompt with token counting - result = prompt.generate(encoding="cl100k") - - # Print the results - print(f"Generated prompt for directory: {result['directory']}") - print(f"Token count: {result['token_count']}") - print(f"Model info: {result['model_info']}") - - # Print the first 1000 characters of the prompt, or less if shorter - print("\nPrompt preview:") - prompt_text = result['prompt'] - if prompt_text: - preview_length = min(1000, len(prompt_text)) - print(f"{prompt_text[:preview_length]}...") - else: - print("No prompt generated") - - # Git operations example - print("\nGit operations:") - - try: - # Get current changes - diff = prompt.get_git_diff() - print("\nCurrent git diff:") - print(diff[:200] + "..." if diff else "No changes") - - # Get diff between branches - branch_diff = prompt.get_git_diff_between_branches("main", "develop") - print("\nDiff between main and develop:") - print(branch_diff[:200] + "..." if branch_diff else "No differences") - - # Get git log - git_log = prompt.get_git_log("main", "develop") - print("\nGit log between main and develop:") - print(git_log[:200] + "..." if git_log else "No log entries") - - except Exception as e: - print(f"Git operations failed: {e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/crates/code2prompt-python/requirements-dev.lock b/crates/code2prompt-python/requirements-dev.lock deleted file mode 100644 index 93fffa55..00000000 --- a/crates/code2prompt-python/requirements-dev.lock +++ /dev/null @@ -1,24 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: false -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -iniconfig==2.1.0 - # via pytest -maturin==1.8.3 -packaging==24.2 - # via pytest -patchelf==0.17.2.2 - # via code2prompt-rs -pip==25.0.1 - # via code2prompt-rs -pluggy==1.5.0 - # via pytest -pytest==8.3.5 diff --git a/crates/code2prompt-python/requirements.lock b/crates/code2prompt-python/requirements.lock deleted file mode 100644 index 042df73b..00000000 --- a/crates/code2prompt-python/requirements.lock +++ /dev/null @@ -1,16 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: false -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -patchelf==0.17.2.2 - # via code2prompt-rs -pip==25.0.1 - # via code2prompt-rs diff --git a/crates/code2prompt-python/src/python.rs b/crates/code2prompt-python/src/python.rs index c492049e..bbb92f6b 100644 --- a/crates/code2prompt-python/src/python.rs +++ b/crates/code2prompt-python/src/python.rs @@ -1,358 +1,789 @@ -use pyo3::prelude::*; use std::collections::HashMap; use std::path::PathBuf; -use code2prompt_core::configuration::Code2PromptConfigBuilder; -use code2prompt_core::session::Code2PromptSession; +use code2prompt_core::analysis::{ + CodebaseAnalysis, EntryMetadata as AnalysisEntryMetadata, ExtensionStat, TokenMapEntry, + TokenMapOptions, +}; +use code2prompt_core::configuration::Code2PromptConfig; +use code2prompt_core::file_processor::{FileProcessorsConfig, IpynbProcessorConfig}; +use code2prompt_core::path::{EntryMetadata, FileEntry}; +use code2prompt_core::session::{Code2PromptSession, RenderedPrompt, SessionData}; use code2prompt_core::sort::FileSortMethod; use code2prompt_core::template::OutputFormat; use code2prompt_core::tokenizer::{TokenFormat, TokenizerType}; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; + +fn runtime_error(error: impl std::fmt::Display) -> PyErr { + PyRuntimeError::new_err(error.to_string()) +} + +// ----------------------------------------------------------------------------- +// Core enums +// ----------------------------------------------------------------------------- + +#[pyclass(name = "OutputFormat", eq, from_py_object)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum PyOutputFormat { + Markdown, + Json, + Xml, +} + +impl From for OutputFormat { + fn from(value: PyOutputFormat) -> Self { + match value { + PyOutputFormat::Markdown => Self::Markdown, + PyOutputFormat::Json => Self::Json, + PyOutputFormat::Xml => Self::Xml, + } + } +} + +impl From for PyOutputFormat { + fn from(value: OutputFormat) -> Self { + match value { + OutputFormat::Markdown => Self::Markdown, + OutputFormat::Json => Self::Json, + OutputFormat::Xml => Self::Xml, + } + } +} + +#[pyclass(name = "TokenizerType", eq, from_py_object)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum PyTokenizerType { + O200kBase, + Cl100kBase, + P50kBase, + P50kEdit, + R50kBase, +} + +impl From for TokenizerType { + fn from(value: PyTokenizerType) -> Self { + match value { + PyTokenizerType::O200kBase => Self::O200kBase, + PyTokenizerType::Cl100kBase => Self::Cl100kBase, + PyTokenizerType::P50kBase => Self::P50kBase, + PyTokenizerType::P50kEdit => Self::P50kEdit, + PyTokenizerType::R50kBase => Self::R50kBase, + } + } +} + +impl From for PyTokenizerType { + fn from(value: TokenizerType) -> Self { + match value { + TokenizerType::O200kBase => Self::O200kBase, + TokenizerType::Cl100kBase => Self::Cl100kBase, + TokenizerType::P50kBase => Self::P50kBase, + TokenizerType::P50kEdit => Self::P50kEdit, + TokenizerType::R50kBase => Self::R50kBase, + } + } +} -#[pyclass(from_py_object)] +#[pyclass(name = "TokenFormat", eq, from_py_object)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum PyTokenFormat { + Raw, + Format, +} + +impl From for TokenFormat { + fn from(value: PyTokenFormat) -> Self { + match value { + PyTokenFormat::Raw => Self::Raw, + PyTokenFormat::Format => Self::Format, + } + } +} + +impl From for PyTokenFormat { + fn from(value: TokenFormat) -> Self { + match value { + TokenFormat::Raw => Self::Raw, + TokenFormat::Format => Self::Format, + } + } +} + +#[pyclass(name = "FileSortMethod", eq, from_py_object)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum PyFileSortMethod { + NameAsc, + NameDesc, + DateAsc, + DateDesc, +} + +impl From for FileSortMethod { + fn from(value: PyFileSortMethod) -> Self { + match value { + PyFileSortMethod::NameAsc => Self::NameAsc, + PyFileSortMethod::NameDesc => Self::NameDesc, + PyFileSortMethod::DateAsc => Self::DateAsc, + PyFileSortMethod::DateDesc => Self::DateDesc, + } + } +} + +impl From for PyFileSortMethod { + fn from(value: FileSortMethod) -> Self { + match value { + FileSortMethod::NameAsc => Self::NameAsc, + FileSortMethod::NameDesc => Self::NameDesc, + FileSortMethod::DateAsc => Self::DateAsc, + FileSortMethod::DateDesc => Self::DateDesc, + } + } +} + +// ----------------------------------------------------------------------------- +// Configuration +// ----------------------------------------------------------------------------- + +#[pyclass(name = "IpynbProcessorConfig", get_all, set_all, from_py_object)] #[derive(Clone)] -struct PyCode2PromptSession { - inner: Code2PromptSession, +struct PyIpynbProcessorConfig { + max_code_cells: usize, + include_outputs: bool, + include_markdown: bool, } #[pymethods] -impl PyCode2PromptSession { +impl PyIpynbProcessorConfig { #[new] - fn new(path: &str) -> PyResult { - let config = Code2PromptConfigBuilder::default() - .path(PathBuf::from(path)) - .build() - .map_err(|e| { - PyErr::new::(format!( - "Failed to create config: {}", - e - )) - })?; - - Ok(Self { - inner: Code2PromptSession::new(config), - }) + #[pyo3(signature = (*, max_code_cells=3, include_outputs=false, include_markdown=false))] + fn new(max_code_cells: usize, include_outputs: bool, include_markdown: bool) -> Self { + Self { + max_code_cells, + include_outputs, + include_markdown, + } } +} - // Configure methods that modify the config - fn include(&mut self, patterns: Vec) -> PyResult> { - let mut config = self.inner.config.clone(); - config.include_patterns = patterns; - self.inner = Code2PromptSession::new(config); +impl From<&PyIpynbProcessorConfig> for IpynbProcessorConfig { + fn from(value: &PyIpynbProcessorConfig) -> Self { + Self { + max_code_cells: value.max_code_cells, + include_outputs: value.include_outputs, + include_markdown: value.include_markdown, + } + } +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +impl From<&IpynbProcessorConfig> for PyIpynbProcessorConfig { + fn from(value: &IpynbProcessorConfig) -> Self { + Self { + max_code_cells: value.max_code_cells, + include_outputs: value.include_outputs, + include_markdown: value.include_markdown, + } } +} - fn exclude(&mut self, patterns: Vec) -> PyResult> { - let mut config = self.inner.config.clone(); - config.exclude_patterns = patterns; - self.inner = Code2PromptSession::new(config); +#[pyclass(name = "FileProcessorsConfig", get_all, set_all, from_py_object)] +#[derive(Clone)] +struct PyFileProcessorsConfig { + ipynb: PyIpynbProcessorConfig, +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +#[pymethods] +impl PyFileProcessorsConfig { + #[new] + #[pyo3(signature = (*, ipynb=None))] + fn new(ipynb: Option) -> Self { + Self { + ipynb: ipynb + .unwrap_or_else(|| PyIpynbProcessorConfig::from(&IpynbProcessorConfig::default())), + } } +} - fn with_line_numbers(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.line_numbers = value; - self.inner = Code2PromptSession::new(config); +impl From<&PyFileProcessorsConfig> for FileProcessorsConfig { + fn from(value: &PyFileProcessorsConfig) -> Self { + Self { + ipynb: IpynbProcessorConfig::from(&value.ipynb), + } + } +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +impl From<&FileProcessorsConfig> for PyFileProcessorsConfig { + fn from(value: &FileProcessorsConfig) -> Self { + Self { + ipynb: PyIpynbProcessorConfig::from(&value.ipynb), + } } +} - fn with_absolute_paths(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.absolute_path = value; - self.inner = Code2PromptSession::new(config); +#[pyclass(name = "Code2PromptConfig", get_all, set_all, from_py_object)] +#[derive(Clone)] +struct PyCode2PromptConfig { + path: PathBuf, + include_patterns: Vec, + exclude_patterns: Vec, + line_numbers: bool, + absolute_path: bool, + full_directory_tree: bool, + no_codeblock: bool, + follow_symlinks: bool, + hidden: bool, + no_ignore: bool, + sort_method: Option, + output_format: PyOutputFormat, + custom_template: Option, + encoding: PyTokenizerType, + token_format: PyTokenFormat, + diff_enabled: bool, + diff_branches: Option<(String, String)>, + log_branches: Option<(String, String)>, + template_name: String, + template_str: String, + user_variables: HashMap, + token_map_enabled: bool, + deselected: bool, + processors: PyFileProcessorsConfig, +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +#[pymethods] +impl PyCode2PromptConfig { + #[new] + #[pyo3(signature = ( + path, + *, + include_patterns=None, + exclude_patterns=None, + line_numbers=false, + absolute_path=false, + full_directory_tree=false, + no_codeblock=false, + follow_symlinks=false, + hidden=false, + no_ignore=false, + sort_method=None, + output_format=None, + custom_template=None, + encoding=None, + token_format=None, + diff_enabled=false, + diff_branches=None, + log_branches=None, + template_name=None, + template_str=None, + user_variables=None, + token_map_enabled=false, + deselected=false, + processors=None, + ))] + #[allow(clippy::too_many_arguments)] + fn new( + path: PathBuf, + include_patterns: Option>, + exclude_patterns: Option>, + line_numbers: bool, + absolute_path: bool, + full_directory_tree: bool, + no_codeblock: bool, + follow_symlinks: bool, + hidden: bool, + no_ignore: bool, + sort_method: Option, + output_format: Option, + custom_template: Option, + encoding: Option, + token_format: Option, + diff_enabled: bool, + diff_branches: Option<(String, String)>, + log_branches: Option<(String, String)>, + template_name: Option, + template_str: Option, + user_variables: Option>, + token_map_enabled: bool, + deselected: bool, + processors: Option, + ) -> Self { + Self { + path, + include_patterns: include_patterns.unwrap_or_default(), + exclude_patterns: exclude_patterns.unwrap_or_default(), + line_numbers, + absolute_path, + full_directory_tree, + no_codeblock, + follow_symlinks, + hidden, + no_ignore, + sort_method, + output_format: output_format.unwrap_or(PyOutputFormat::Markdown), + custom_template, + encoding: encoding.unwrap_or(PyTokenizerType::Cl100kBase), + token_format: token_format.unwrap_or(PyTokenFormat::Raw), + diff_enabled, + diff_branches, + log_branches, + template_name: template_name.unwrap_or_default(), + template_str: template_str.unwrap_or_default(), + user_variables: user_variables.unwrap_or_default(), + token_map_enabled, + deselected, + processors: processors + .unwrap_or_else(|| PyFileProcessorsConfig::from(&FileProcessorsConfig::default())), + } } +} - fn with_full_directory_tree(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.full_directory_tree = value; - self.inner = Code2PromptSession::new(config); +impl From<&PyCode2PromptConfig> for Code2PromptConfig { + fn from(value: &PyCode2PromptConfig) -> Self { + Self { + path: value.path.clone(), + include_patterns: value.include_patterns.clone(), + exclude_patterns: value.exclude_patterns.clone(), + line_numbers: value.line_numbers, + absolute_path: value.absolute_path, + full_directory_tree: value.full_directory_tree, + no_codeblock: value.no_codeblock, + follow_symlinks: value.follow_symlinks, + entity_map: false, + hidden: value.hidden, + no_ignore: value.no_ignore, + sort_method: value.sort_method.map(Into::into), + output_format: value.output_format.into(), + custom_template: value.custom_template.clone(), + encoding: value.encoding.into(), + token_format: value.token_format.into(), + diff_enabled: value.diff_enabled, + diff_branches: value.diff_branches.clone(), + diff_files: None, + log_branches: value.log_branches.clone(), + template_name: value.template_name.clone(), + template_str: value.template_str.clone(), + user_variables: value.user_variables.clone(), + token_map_enabled: value.token_map_enabled, + deselected: value.deselected, + processors: FileProcessorsConfig::from(&value.processors), + } + } +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +impl From<&Code2PromptConfig> for PyCode2PromptConfig { + fn from(value: &Code2PromptConfig) -> Self { + Self { + path: value.path.clone(), + include_patterns: value.include_patterns.clone(), + exclude_patterns: value.exclude_patterns.clone(), + line_numbers: value.line_numbers, + absolute_path: value.absolute_path, + full_directory_tree: value.full_directory_tree, + no_codeblock: value.no_codeblock, + follow_symlinks: value.follow_symlinks, + hidden: value.hidden, + no_ignore: value.no_ignore, + sort_method: value.sort_method.map(Into::into), + output_format: value.output_format.into(), + custom_template: value.custom_template.clone(), + encoding: value.encoding.into(), + token_format: value.token_format.into(), + diff_enabled: value.diff_enabled, + diff_branches: value.diff_branches.clone(), + log_branches: value.log_branches.clone(), + template_name: value.template_name.clone(), + template_str: value.template_str.clone(), + user_variables: value.user_variables.clone(), + token_map_enabled: value.token_map_enabled, + deselected: value.deselected, + processors: PyFileProcessorsConfig::from(&value.processors), + } } +} - fn with_code_blocks(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.no_codeblock = !value; // Invert because API is different - self.inner = Code2PromptSession::new(config); +// ----------------------------------------------------------------------------- +// Session data and generated results +// ----------------------------------------------------------------------------- - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +#[pyclass(name = "EntryMetadata", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PyEntryMetadata { + is_dir: bool, + is_symlink: bool, +} + +impl From for PyEntryMetadata { + fn from(value: EntryMetadata) -> Self { + Self { + is_dir: value.is_dir, + is_symlink: value.is_symlink, + } } +} - fn follow_symlinks(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.follow_symlinks = value; - self.inner = Code2PromptSession::new(config); +#[pyclass(name = "FileEntry", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PyFileEntry { + path: String, + extension: String, + code: String, + token_count: usize, + metadata: PyEntryMetadata, + mod_time: Option, +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +impl From<&FileEntry> for PyFileEntry { + fn from(value: &FileEntry) -> Self { + Self { + path: value.path.clone(), + extension: value.extension.clone(), + code: value.code.clone(), + token_count: value.token_count, + metadata: value.metadata.into(), + mod_time: value.mod_time, + } } +} - fn include_hidden(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.hidden = value; - self.inner = Code2PromptSession::new(config); +#[pyclass(name = "SessionData", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PySessionData { + absolute_code_path: Option, + source_tree: Option, + files: Option>, + git_diff: Option, + git_diff_branch: Option, + git_log_branch: Option, +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +impl From<&SessionData> for PySessionData { + fn from(value: &SessionData) -> Self { + Self { + absolute_code_path: value.absolute_code_path.clone(), + source_tree: value.source_tree.clone(), + files: value + .files + .as_deref() + .map(|files| files.iter().map(PyFileEntry::from).collect()), + git_diff: value.git_diff.clone(), + git_diff_branch: value.git_diff_branch.clone(), + git_log_branch: value.git_log_branch.clone(), + } + } +} + +#[pyclass(name = "RenderedPrompt", frozen, from_py_object)] +#[derive(Clone)] +struct PyRenderedPrompt { + inner: RenderedPrompt, +} + +#[pymethods] +impl PyRenderedPrompt { + #[getter] + fn prompt(&self) -> &str { + &self.inner.prompt } - fn no_ignore(&mut self, value: bool) -> PyResult> { - let mut config = self.inner.config.clone(); - config.no_ignore = value; - self.inner = Code2PromptSession::new(config); + #[getter] + fn directory_name(&self) -> &str { + &self.inner.directory_name + } - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) + #[getter] + fn token_count(&self) -> usize { + self.inner.token_count } - fn sort_by(&mut self, method: &str) -> PyResult> { - let mut config = self.inner.config.clone(); - match method.to_lowercase().as_str() { - "name" | "name_asc" => config.sort_method = Some(FileSortMethod::NameAsc), - "name_desc" => config.sort_method = Some(FileSortMethod::NameDesc), - "date" | "date_asc" => config.sort_method = Some(FileSortMethod::DateAsc), - "date_desc" => config.sort_method = Some(FileSortMethod::DateDesc), - _ => { - return Err(PyErr::new::(format!( - "Invalid sort method: {}. Valid values: name_asc, name_desc, date_asc, date_desc", - method - ))); - } + #[getter] + fn model_info(&self) -> &str { + self.inner.model_info + } + + #[getter] + fn files(&self) -> Vec { + self.inner.files.clone() + } +} + +impl From for PyRenderedPrompt { + fn from(value: RenderedPrompt) -> Self { + Self { inner: value } + } +} + +// ----------------------------------------------------------------------------- +// Analysis +// ----------------------------------------------------------------------------- + +#[pyclass(name = "TokenMapOptions", get_all, set_all, from_py_object)] +#[derive(Clone)] +struct PyTokenMapOptions { + max_lines: usize, + min_percent: f64, +} + +#[pymethods] +impl PyTokenMapOptions { + #[new] + #[pyo3(signature = (*, max_lines=20, min_percent=0.1))] + fn new(max_lines: usize, min_percent: f64) -> Self { + Self { + max_lines, + min_percent, } - self.inner = Code2PromptSession::new(config); - - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) } +} - fn output_format(&mut self, format: &str) -> PyResult> { - let mut config = self.inner.config.clone(); - match format.to_lowercase().as_str() { - "markdown" => config.output_format = OutputFormat::Markdown, - // Assuming from the error that there's a Plain variant - please replace if needed - "xml" | "text" => config.output_format = OutputFormat::Xml, - "json" => config.output_format = OutputFormat::Json, - _ => { - return Err(PyErr::new::(format!( - "Invalid output format: {}", - format - ))); - } +impl From<&PyTokenMapOptions> for TokenMapOptions { + fn from(value: &PyTokenMapOptions) -> Self { + Self { + max_lines: value.max_lines, + min_percent: value.min_percent, } - self.inner = Code2PromptSession::new(config); - - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) } +} - fn with_token_encoding(&mut self, encoding: &str) -> PyResult> { - let mut config = self.inner.config.clone(); - match encoding.to_lowercase().as_str() { - "cl100k" => config.encoding = TokenizerType::Cl100kBase, - "o200k" => config.encoding = TokenizerType::O200kBase, - "p50k" => config.encoding = TokenizerType::P50kBase, - "p50k_edit" => config.encoding = TokenizerType::P50kEdit, - "r50k" => config.encoding = TokenizerType::R50kBase, - _ => { - return Err(PyErr::new::(format!( - "Invalid token encoding: {}", - encoding - ))); - } +#[pyclass(name = "TokenMapEntryMetadata", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PyTokenMapEntryMetadata { + is_dir: bool, +} + +impl From for PyTokenMapEntryMetadata { + fn from(value: AnalysisEntryMetadata) -> Self { + Self { + is_dir: value.is_dir, } - self.inner = Code2PromptSession::new(config); - - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) } +} - fn with_token_format(&mut self, format: &str) -> PyResult> { - let mut config = self.inner.config.clone(); - match format.to_lowercase().as_str() { - "raw" => config.token_format = TokenFormat::Raw, - "format" => config.token_format = TokenFormat::Format, - _ => { - return Err(PyErr::new::(format!( - "Invalid token format: {}. Use 'raw' or 'format'.", - format - ))); - } +#[pyclass(name = "TokenMapEntry", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PyTokenMapEntry { + path: String, + name: String, + tokens: usize, + percentage: f64, + depth: usize, + is_last_child: bool, + has_children: bool, + metadata: PyTokenMapEntryMetadata, +} + +impl From for PyTokenMapEntry { + fn from(value: TokenMapEntry) -> Self { + Self { + path: value.path, + name: value.name, + tokens: value.tokens, + percentage: value.percentage, + depth: value.depth, + is_last_child: value.is_last_child, + has_children: value.has_children, + metadata: value.metadata.into(), } - self.inner = Code2PromptSession::new(config); - - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) } +} + +#[pyclass(name = "ExtensionStat", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PyExtensionStat { + extension: String, + file_count: usize, + tokens: usize, + percentage: f64, +} - #[pyo3(signature = (template, name=None))] - fn with_template(&mut self, template: String, name: Option) -> PyResult> { - let mut config = self.inner.config.clone(); - config.template_str = template; - if let Some(name_val) = name { - config.template_name = name_val; - } else { - config.template_name = "custom".to_string(); +impl From for PyExtensionStat { + fn from(value: ExtensionStat) -> Self { + Self { + extension: value.extension, + file_count: value.file_count, + tokens: value.tokens, + percentage: value.percentage, } - self.inner = Code2PromptSession::new(config); - - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) } +} - #[pyo3(signature = (key, value))] - fn with_variable(&mut self, key: String, value: String) -> PyResult> { - let mut config = self.inner.config.clone(); - config.user_variables.insert(key, value); - self.inner = Code2PromptSession::new(config); +#[pyclass(name = "CodebaseAnalysis", frozen)] +struct PyCodebaseAnalysis { + files: Vec, + total_tokens: usize, +} - Python::attach(|py| { - Ok(Py::new( - py, - Self { - inner: self.inner.clone(), - }, - )?) - }) +#[pymethods] +impl PyCodebaseAnalysis { + fn token_map(&self, options: &PyTokenMapOptions) -> Vec { + CodebaseAnalysis::new(&self.files, self.total_tokens) + .token_map(options.into()) + .into_iter() + .map(Into::into) + .collect() } - fn generate(&mut self) -> PyResult { - match self.inner.generate_prompt() { - Ok(rendered) => Ok(rendered.prompt), - Err(e) => Err(PyErr::new::(format!( - "Failed to generate prompt: {}", - e - ))), - } + fn by_extension(&self) -> Vec { + CodebaseAnalysis::new(&self.files, self.total_tokens) + .by_extension() + .into_iter() + .map(Into::into) + .collect() + } + + fn raw_files(&self) -> Vec { + self.files.iter().map(PyFileEntry::from).collect() } +} + +// ----------------------------------------------------------------------------- +// Stateful session +// ----------------------------------------------------------------------------- + +#[pyclass(name = "Code2PromptSession")] +struct PyCode2PromptSession { + inner: Code2PromptSession, +} - fn info(&self) -> PyResult> { - // Since there's no direct info() method, we'll create a simple info map - let mut info = HashMap::new(); - info.insert( - "path".to_string(), - self.inner.config.path.to_string_lossy().to_string(), - ); - info.insert( - "include_patterns".to_string(), - format!("{:?}", self.inner.config.include_patterns), - ); - info.insert( - "exclude_patterns".to_string(), - format!("{:?}", self.inner.config.exclude_patterns), - ); - - Ok(info) - } - - fn token_count(&self) -> PyResult { - // Generate the prompt and count tokens - match self.inner.clone().generate_prompt() { - Ok(rendered) => Ok(rendered.token_count), - Err(e) => Err(PyErr::new::(format!( - "Failed to count tokens: {}", - e - ))), +#[pymethods] +impl PyCode2PromptSession { + #[new] + fn new(config: &PyCode2PromptConfig) -> Self { + Self { + inner: Code2PromptSession::new(config.into()), } } + + #[getter] + fn config(&self) -> PyCode2PromptConfig { + PyCode2PromptConfig::from(&self.inner.config) + } + + #[getter] + fn data(&self) -> PySessionData { + PySessionData::from(&self.inner.data) + } + + fn add_include_pattern<'py>( + mut slf: PyRefMut<'py, Self>, + pattern: String, + ) -> PyRefMut<'py, Self> { + slf.inner.add_include_pattern(pattern); + slf + } + + fn add_exclude_pattern<'py>( + mut slf: PyRefMut<'py, Self>, + pattern: String, + ) -> PyRefMut<'py, Self> { + slf.inner.add_exclude_pattern(pattern); + slf + } + + fn select_file<'py>(mut slf: PyRefMut<'py, Self>, path: PathBuf) -> PyRefMut<'py, Self> { + slf.inner.select_file(path); + slf + } + + fn deselect_file<'py>(mut slf: PyRefMut<'py, Self>, path: PathBuf) -> PyRefMut<'py, Self> { + slf.inner.deselect_file(path); + slf + } + + fn toggle_file_selection<'py>( + mut slf: PyRefMut<'py, Self>, + path: PathBuf, + ) -> PyRefMut<'py, Self> { + slf.inner.toggle_file_selection(path); + slf + } + + fn is_file_selected(&mut self, path: PathBuf) -> bool { + self.inner.is_file_selected(&path) + } + + fn get_selected_files(&mut self) -> PyResult> { + self.inner.get_selected_files().map_err(runtime_error) + } + + fn clear_user_actions<'py>(mut slf: PyRefMut<'py, Self>) -> PyRefMut<'py, Self> { + slf.inner.clear_user_actions(); + slf + } + + fn has_user_actions(&self) -> bool { + self.inner.has_user_actions() + } + + fn set_deselected<'py>(mut slf: PyRefMut<'py, Self>, value: bool) -> PyRefMut<'py, Self> { + slf.inner.set_deselected(value); + slf + } + + fn load_codebase(&mut self) -> PyResult<()> { + self.inner.load_codebase().map_err(runtime_error) + } + + fn load_git_diff(&mut self) -> PyResult<()> { + self.inner.load_git_diff().map_err(runtime_error) + } + + fn load_git_diff_between_branches(&mut self) -> PyResult<()> { + self.inner + .load_git_diff_between_branches() + .map_err(runtime_error) + } + + fn load_git_log_between_branches(&mut self) -> PyResult<()> { + self.inner + .load_git_log_between_branches() + .map_err(runtime_error) + } + + fn raw_analysis(&self) -> Option { + self.inner.raw_analysis().map(|analysis| { + let files = analysis.raw_files().to_vec(); + let total_tokens = files.iter().map(|file| file.token_count).sum(); + PyCodebaseAnalysis { + files, + total_tokens, + } + }) + } + + fn contextual_analysis(&self, prompt: &PyRenderedPrompt) -> Option { + self.inner + .contextual_analysis(&prompt.inner) + .map(|analysis| PyCodebaseAnalysis { + files: analysis.raw_files().to_vec(), + total_tokens: prompt.inner.token_count, + }) + } + + fn generate_prompt(&mut self) -> PyResult { + self.inner + .generate_prompt() + .map(Into::into) + .map_err(runtime_error) + } } -// Module definition - Updated PyO3 syntax #[pymodule(name = "code2prompt_rs")] -fn code2prompt_rs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn code2prompt_rs(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/crates/code2prompt-python/src/python.rs.bak b/crates/code2prompt-python/src/python.rs.bak deleted file mode 100644 index e6954b65..00000000 --- a/crates/code2prompt-python/src/python.rs.bak +++ /dev/null @@ -1,195 +0,0 @@ -use pyo3::prelude::*; -use pyo3::types::PyDict; -use std::path::PathBuf; - -use code2prompt_core::{ - git::{get_git_diff, get_git_diff_between_branches, get_git_log}, - path::traverse_directory, - template::{handlebars_setup, render_template}, - tokenizer::{count_tokens, TokenizerType}, -}; - -/// Python module for code2prompt -#[pymodule(name = "code2prompt_rs")] -fn code2prompt_rs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) -} - -/// Main class for generating prompts from code -#[pyclass] -struct Code2Prompt { - path: PathBuf, - include_patterns: Vec, - exclude_patterns: Vec, - include_priority: bool, - line_numbers: bool, - relative_paths: bool, - exclude_from_tree: bool, - no_codeblock: bool, - follow_symlinks: bool, - hidden: bool, - no_ignore: bool, -} - -#[pymethods] -impl Code2Prompt { - /// Create a new Code2Prompt instance - /// - /// Args: - /// path (str): Path to the codebase directory - /// include_patterns (List[str], optional): Patterns to include. Defaults to []. - /// exclude_patterns (List[str], optional): Patterns to exclude. Defaults to []. - /// include_priority (bool, optional): Give priority to include patterns. Defaults to False. - /// line_numbers (bool, optional): Add line numbers to code. Defaults to False. - /// relative_paths (bool, optional): Use relative paths. Defaults to False. - /// exclude_from_tree (bool, optional): Exclude files from tree based on patterns. Defaults to False. - /// no_codeblock (bool, optional): Don't wrap code in markdown blocks. Defaults to False. - /// follow_symlinks (bool, optional): Follow symbolic links. Defaults to False. - /// hidden (bool, optional): Include hidden directories and files. Defaults to False. - /// no_ignore (bool, optional): Skip .gitignore rules. Defaults to False. - #[new] - #[pyo3(signature = ( - path, - include_patterns = vec![], - exclude_patterns = vec![], - include_priority = false, - line_numbers = false, - relative_paths = false, - exclude_from_tree = false, - no_codeblock = false, - follow_symlinks = false, - hidden = false, - no_ignore = false, - ))] - fn new( - path: String, - include_patterns: Vec, - exclude_patterns: Vec, - include_priority: bool, - line_numbers: bool, - relative_paths: bool, - exclude_from_tree: bool, - no_codeblock: bool, - follow_symlinks: bool, - hidden: bool, - no_ignore: bool, - ) -> Self { - Self { - path: PathBuf::from(path), - include_patterns, - exclude_patterns, - include_priority, - line_numbers, - relative_paths, - exclude_from_tree, - no_codeblock, - follow_symlinks, - hidden, - no_ignore, - } - } - - /// Generate a prompt from the codebase - /// - /// Args: - /// template (str, optional): Custom Handlebars template. Defaults to None. - /// encoding (str, optional): Token encoding to use. Defaults to "cl100k". - /// - /// Returns: - /// dict: Dictionary containing the rendered prompt and metadata - #[pyo3(signature = (template=None, encoding=None))] - fn generate(&self, template: Option, encoding: Option) -> PyResult { - Python::with_gil(|py| { - // Traverse directory - let (tree, files) = traverse_directory( - &self.path, - &self.include_patterns, - &self.exclude_patterns, - self.include_priority, - self.line_numbers, - self.relative_paths, - self.exclude_from_tree, - self.no_codeblock, - self.follow_symlinks, - self.hidden, - self.no_ignore, - None, - ) - .map_err(|e| PyErr::new::(e.to_string()))?; - - // Setup template - let template_content = template - .unwrap_or_else(|| include_str!("../../default_template_md.hbs").to_string()); - let handlebars = handlebars_setup(&template_content, "template") - .map_err(|e| PyErr::new::(e.to_string()))?; - - // Prepare data - let data = serde_json::json!({ - "absolute_code_path": self.path.display().to_string(), - "source_tree": tree, - "files": files, - }); - - // Render template - let rendered = render_template(&handlebars, "template", &data) - .map_err(|e| PyErr::new::(e.to_string()))?; - - // Select tokenizer type - let tokenizer_type = encoding - .as_deref() - .unwrap_or("cl100k") - .parse::() - .unwrap_or(TokenizerType::Cl100kBase); // Fallback to `cl100k` - - let model_info = tokenizer_type.description(); - - // Count tokens - let token_count = count_tokens(&rendered, &tokenizer_type); - - // Create return dictionary - let result = PyDict::new(py); - result.set_item("prompt", rendered)?; - result.set_item("directory", self.path.display().to_string())?; - result.set_item("token_count", token_count)?; - result.set_item("model_info", model_info)?; - - Ok(result.into()) - }) - } - - /// Get git diff for the repository - /// - /// Returns: - /// str: Git diff output - fn get_git_diff(&self) -> PyResult { - get_git_diff(&self.path) - .map_err(|e| PyErr::new::(e.to_string())) - } - - /// Get git diff between two branches - /// - /// Args: - /// branch1 (str): First branch name - /// branch2 (str): Second branch name - /// - /// Returns: - /// str: Git diff output - fn get_git_diff_between_branches(&self, branch1: &str, branch2: &str) -> PyResult { - get_git_diff_between_branches(&self.path, branch1, branch2) - .map_err(|e| PyErr::new::(e.to_string())) - } - - /// Get git log between two branches - /// - /// Args: - /// branch1 (str): First branch name - /// branch2 (str): Second branch name - /// - /// Returns: - /// str: Git log output - fn get_git_log(&self, branch1: &str, branch2: &str) -> PyResult { - get_git_log(&self.path, branch1, branch2) - .map_err(|e| PyErr::new::(e.to_string())) - } -} diff --git a/crates/code2prompt-python/tests/conftest.py b/crates/code2prompt-python/tests/conftest.py index fdfd62dd..ce37b759 100644 --- a/crates/code2prompt-python/tests/conftest.py +++ b/crates/code2prompt-python/tests/conftest.py @@ -1,57 +1,75 @@ -"""Pytest fixtures for code2prompt tests.""" -import os -import pytest -import tempfile -import shutil +"""Shared fixtures for the native Python bindings.""" + +import json +import subprocess from pathlib import Path -@pytest.fixture(scope="module") -def test_hierarchy(): - """Create a test hierarchy of files and directories.""" - # Create a temporary directory - temp_dir = tempfile.mkdtemp() - - try: - # Create directories - lowercase_dir = Path(temp_dir) / "lowercase" - uppercase_dir = Path(temp_dir) / "uppercase" - secret_dir = Path(temp_dir) / ".secret" - - for dir_path in [lowercase_dir, uppercase_dir, secret_dir]: - dir_path.mkdir(parents=True, exist_ok=True) - - # Create files - files = [ - ("lowercase/foo.py", "def foo():\n return 'foo'\n"), - ("lowercase/bar.py", "def bar():\n return 'bar'\n"), - ("lowercase/baz.py", "def baz():\n return 'baz'\n"), - ("lowercase/qux.txt", "content qux.txt"), - ("lowercase/corge.txt", "content corge.txt"), - ("lowercase/grault.txt", "content grault.txt"), - ("uppercase/FOO.py", "def FOO():\n return 'FOO'\n"), - ("uppercase/BAR.py", "def BAR():\n return 'BAR'\n"), - ("uppercase/BAZ.py", "def BAZ():\n return 'BAZ'\n"), - ("uppercase/QUX.txt", "CONTENT QUX.TXT"), - ("uppercase/CORGE.txt", "CONTENT CORGE.TXT"), - ("uppercase/GRAULT.txt", "CONTENT GRAULT.TXT"), - (".secret/secret.txt", "SECRET"), +import pytest + + +@pytest.fixture() +def project(tmp_path: Path) -> Path: + """Create a small repository-like tree with ignored and hidden files.""" + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + (tmp_path / ".secret").mkdir() + + (tmp_path / "src" / "main.py").write_text( + "def main():\n return 'main'\n", encoding="utf-8" + ) + (tmp_path / "src" / "utils.py").write_text( + "def helper():\n return 42\n", encoding="utf-8" + ) + (tmp_path / "tests" / "test_main.py").write_text( + "def test_main():\n assert True\n", encoding="utf-8" + ) + (tmp_path / "README.md").write_text("# Test project\n", encoding="utf-8") + (tmp_path / "ignored.txt").write_text("ignored by gitignore\n", encoding="utf-8") + (tmp_path / ".secret" / "secret.py").write_text("SECRET = True\n", encoding="utf-8") + (tmp_path / ".gitignore").write_text("*.txt\n", encoding="utf-8") + + notebook = { + "cells": [ + {"cell_type": "markdown", "source": ["# Notes\n"]}, + { + "cell_type": "code", + "source": ["print('hello')\n"], + "outputs": [ + {"output_type": "stream", "name": "stdout", "text": ["hello\n"]} + ], + }, + {"cell_type": "code", "source": ["x = 2\n"], "outputs": []}, ] - - for file_path, content in files: - full_path = Path(temp_dir) / file_path - full_path.write_text(content) - - # Create a gitignore file - gitignore_path = Path(temp_dir) / ".gitignore" - gitignore_path.write_text("*.txt\n") - - # Return the path - yield temp_dir - finally: - # Clean up - shutil.rmtree(temp_dir) - -@pytest.fixture -def test_dir(test_hierarchy): - """Return the path to the test hierarchy.""" - return test_hierarchy \ No newline at end of file + } + (tmp_path / "notebook.ipynb").write_text(json.dumps(notebook), encoding="utf-8") + run_git(tmp_path, "init") + return tmp_path + + +def run_git(repository: Path, *args: str) -> str: + return subprocess.run( + ["git", *args], + cwd=repository, + check=True, + capture_output=True, + text=True, + ).stdout + + +@pytest.fixture() +def git_project(tmp_path: Path) -> Path: + """Create a two-branch Git repository for session Git tests.""" + run_git(tmp_path, "init", "-b", "main") + run_git(tmp_path, "config", "user.name", "Code2Prompt Tests") + run_git(tmp_path, "config", "user.email", "tests@code2prompt.dev") + + (tmp_path / "changed.py").write_text("VALUE = 1\n", encoding="utf-8") + (tmp_path / "unchanged.py").write_text("STABLE = True\n", encoding="utf-8") + run_git(tmp_path, "add", ".") + run_git(tmp_path, "commit", "-m", "initial") + + run_git(tmp_path, "switch", "-c", "feature") + (tmp_path / "changed.py").write_text("VALUE = 2\n", encoding="utf-8") + run_git(tmp_path, "add", "changed.py") + run_git(tmp_path, "commit", "-m", "change value") + return tmp_path diff --git a/crates/code2prompt-python/tests/test_analysis.py b/crates/code2prompt-python/tests/test_analysis.py new file mode 100644 index 00000000..7cf97762 --- /dev/null +++ b/crates/code2prompt-python/tests/test_analysis.py @@ -0,0 +1,48 @@ +"""Tests for owned Python projections of core analysis results.""" + +from code2prompt_rs import ( + Code2PromptConfig, + Code2PromptSession, + CodebaseAnalysis, + ExtensionStat, + TokenMapEntry, + TokenMapOptions, +) + + +def test_raw_and_contextual_analysis(project): + session = Code2PromptSession( + Code2PromptConfig(str(project), include_patterns=["**/*.py"]) + ) + session.load_codebase() + + raw = session.raw_analysis() + assert isinstance(raw, CodebaseAnalysis) + assert all(file.token_count > 0 for file in raw.raw_files()) + + rendered = session.generate_prompt() + contextual = session.contextual_analysis(rendered) + assert isinstance(contextual, CodebaseAnalysis) + assert len(contextual.raw_files()) == len(rendered.files) + + +def test_extension_and_token_map_models(project): + session = Code2PromptSession( + Code2PromptConfig( + str(project), + include_patterns=["**/*.py", "README.md"], + ) + ) + session.load_codebase() + analysis = session.raw_analysis() + assert analysis is not None + + extension_stats = analysis.by_extension() + assert all(isinstance(item, ExtensionStat) for item in extension_stats) + assert {item.extension for item in extension_stats} == {"py", "md"} + + entries = analysis.token_map(TokenMapOptions(max_lines=4, min_percent=0.0)) + assert entries + assert all(isinstance(item, TokenMapEntry) for item in entries) + assert all(item.tokens >= 0 for item in entries) + assert all(isinstance(item.metadata.is_dir, bool) for item in entries) diff --git a/crates/code2prompt-python/tests/test_config.py b/crates/code2prompt-python/tests/test_config.py index ac8ec5e2..3d91d883 100644 --- a/crates/code2prompt-python/tests/test_config.py +++ b/crates/code2prompt-python/tests/test_config.py @@ -1,61 +1,101 @@ -"""Tests for Code2Prompt configuration.""" +"""Tests for direct bindings of core configuration types.""" + +import code2prompt_rs import pytest -from pathlib import Path -from code2prompt_rs import Code2Prompt - -def test_basic_initialization(test_dir): - """Test that Code2Prompt can be initialized with minimal settings.""" - prompt = Code2Prompt(path=test_dir) - assert prompt is not None - assert str(prompt.path) == test_dir - assert prompt.include_patterns == [] - assert prompt.exclude_patterns == [] - -def test_initialization_with_options(test_dir): - """Test initialization with various options.""" - prompt = Code2Prompt( - path=test_dir, - include_patterns=["*.py"], - exclude_patterns=["**/uppercase/*"], - include_priority=True, +from code2prompt_rs import ( + Code2PromptConfig, + Code2PromptSession, + FileProcessorsConfig, + FileSortMethod, + IpynbProcessorConfig, + OutputFormat, + TokenFormat, + TokenizerType, +) + + +def test_core_defaults_are_exposed(project): + config = Code2PromptConfig(project) + + assert config.path == project + assert config.include_patterns == [] + assert config.exclude_patterns == [] + assert config.output_format == OutputFormat.Markdown + assert config.encoding == TokenizerType.Cl100kBase + assert config.token_format == TokenFormat.Raw + assert config.sort_method is None + assert config.processors.ipynb.max_code_cells == 3 + assert config.processors.ipynb.include_outputs is False + assert config.processors.ipynb.include_markdown is False + + +def test_all_user_facing_core_options_round_trip_through_session(project): + processors = FileProcessorsConfig( + ipynb=IpynbProcessorConfig( + max_code_cells=7, + include_outputs=True, + include_markdown=True, + ) + ) + config = Code2PromptConfig( + str(project), + include_patterns=["**/*.py"], + exclude_patterns=["**/tests/**"], line_numbers=True, - absolute_paths=True, + absolute_path=True, full_directory_tree=True, - code_blocks=False, - follow_symlinks=True + no_codeblock=True, + follow_symlinks=True, + hidden=True, + no_ignore=True, + sort_method=FileSortMethod.NameDesc, + output_format=OutputFormat.Xml, + custom_template="custom-template-path", + encoding=TokenizerType.O200kBase, + token_format=TokenFormat.Format, + diff_enabled=True, + diff_branches=("main", "feature"), + log_branches=("main", "feature"), + template_name="custom", + template_str="{{absolute_code_path}}", + user_variables={"audience": "maintainers"}, + token_map_enabled=True, + deselected=True, + processors=processors, ) - - assert prompt.include_patterns == ["*.py"] - assert prompt.exclude_patterns == ["**/uppercase/*"] - assert prompt.include_priority is True - assert prompt.line_numbers is True - assert prompt.absolute_paths is True - assert prompt.full_directory_tree is True - assert prompt.code_blocks is False - assert prompt.follow_symlinks is True - -def test_session_creation(test_dir): - """Test that a session can be created.""" - prompt = Code2Prompt(path=test_dir) - session = prompt.session() - assert session is not None - - # Verify that the session contains expected info - info = session.info() - assert "path" in info - assert Path(info["path"]) == Path(test_dir) - -def test_configuration_chain(test_dir): - """Test using session for complex configuration.""" - prompt = Code2Prompt(path=test_dir) - session = prompt.session() - - # Apply multiple configurations (using the original session would - # involve setting up method calls to return 'self') - session = session.include(["*.py"]) - session = session.exclude(["**/uppercase/*"]) - session = session.with_line_numbers(True) - - # Verify configuration was applied - info = session.info() - assert info["include_patterns"] != "[]" \ No newline at end of file + + snapshot = Code2PromptSession(config).config + assert snapshot.include_patterns == ["**/*.py"] + assert snapshot.exclude_patterns == ["**/tests/**"] + assert snapshot.line_numbers is True + assert snapshot.absolute_path is True + assert snapshot.full_directory_tree is True + assert snapshot.no_codeblock is True + assert snapshot.follow_symlinks is True + assert snapshot.hidden is True + assert snapshot.no_ignore is True + assert snapshot.sort_method == FileSortMethod.NameDesc + assert snapshot.output_format == OutputFormat.Xml + assert snapshot.custom_template == "custom-template-path" + assert snapshot.encoding == TokenizerType.O200kBase + assert snapshot.token_format == TokenFormat.Format + assert snapshot.diff_enabled is True + assert snapshot.diff_branches == ("main", "feature") + assert snapshot.log_branches == ("main", "feature") + assert snapshot.template_name == "custom" + assert snapshot.template_str == "{{absolute_code_path}}" + assert snapshot.user_variables == {"audience": "maintainers"} + assert snapshot.token_map_enabled is True + assert snapshot.deselected is True + assert snapshot.processors.ipynb.max_code_cells == 7 + + +def test_configuration_requires_native_enums(project): + with pytest.raises(TypeError): + Code2PromptConfig(str(project), output_format="xml") + + +def test_removed_v3_facade_is_not_exported(): + assert not hasattr(code2prompt_rs, "Code2Prompt") + assert hasattr(code2prompt_rs, "Code2PromptConfig") + assert hasattr(code2prompt_rs, "Code2PromptSession") diff --git a/crates/code2prompt-python/tests/test_generation.py b/crates/code2prompt-python/tests/test_generation.py index 65587757..46e95de1 100644 --- a/crates/code2prompt-python/tests/test_generation.py +++ b/crates/code2prompt-python/tests/test_generation.py @@ -1,146 +1,110 @@ -"""Tests for prompt generation.""" -import pytest -from code2prompt_rs import Code2Prompt - -def test_generate_basic(test_dir): - """Test basic prompt generation.""" - prompt = Code2Prompt(path=test_dir) - result = prompt.generate() - - # Basic checks - assert result.prompt is not None +"""Tests for prompt generation through the bound Rust session.""" + +import json + +from code2prompt_rs import ( + Code2PromptConfig, + Code2PromptSession, + OutputFormat, + RenderedPrompt, + TokenizerType, +) + + +def generate(project, **options): + return Code2PromptSession(Code2PromptConfig(str(project), **options)).generate_prompt() + + +def test_generate_prompt_returns_core_result(project): + result = generate(project, include_patterns=["**/*.py"]) + + assert isinstance(result, RenderedPrompt) assert isinstance(result.prompt, str) - assert result.token_count >= 0 - assert str(result.directory) == test_dir - -def test_generate_with_include_patterns(test_dir): - """Test generation with include patterns.""" - prompt = Code2Prompt( - path=test_dir, - include_patterns=["*.py"] - ) - result = prompt.generate() - - # Check that Python files are included - assert "foo.py" in result.prompt - assert "bar.py" in result.prompt - - # Check that text files are excluded - assert "qux.txt" not in result.prompt - assert "corge.txt" not in result.prompt - -def test_generate_with_exclude_patterns(test_dir): - """Test generation with exclude patterns.""" - prompt = Code2Prompt( - path=test_dir, - exclude_patterns=["**/uppercase/*"] - ) - result = prompt.generate() - - # Check that uppercase directory files are excluded - assert "FOO.py" not in result.prompt - assert "BAR.py" not in result.prompt - - # Check that lowercase directory files are included - assert "foo.py" in result.prompt or "lowercase/foo.py" in result.prompt - -def test_generate_with_line_numbers(test_dir): - """Test generation with line numbers.""" - prompt = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"], - line_numbers=True + assert result.token_count > 0 + assert result.directory_name == project.name + assert result.model_info + assert sorted(result.files) == ["src/main.py", "src/utils.py", "tests/test_main.py"] + + +def test_filtering_hidden_ignore_and_path_modes(project): + default_result = generate(project) + assert "secret.py" not in default_result.prompt + assert "ignored.txt" not in default_result.files + + expanded_result = generate( + project, + hidden=True, + no_ignore=True, + absolute_path=True, + include_patterns=["**/*.py", "**/*.txt"], ) - result = prompt.generate() - - # Check for line numbers in output (either format 1: or 1.|) - assert "1:" in result.prompt or "1 |" in result.prompt - -def test_generate_with_relative_and_absolute_paths(test_dir): - """Test generation with absolute paths.""" - prompt_absolute = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"], - absolute_paths=True - ) - result = prompt_absolute.generate() - - # Should include absolute path format - assert test_dir in result.prompt - - # Should include absolute path - assert "lowercase/foo.py" in result.prompt - - prompt_relative = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"], - absolute_paths=False - ) - result = prompt_relative.generate() - - # Should not include absolute path format - assert test_dir not in result.prompt - - # Should include absolute path - assert "lowercase/foo.py" in result.prompt - -def test_generate_with_custom_template(test_dir): - """Test generation with custom template.""" - template = """# Code Overview - {% for file in files %} - ## {{ file.path }} - ```{{ file.language }} - {{ file.content }}" \ - "{% endfor %}""" - - prompt = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"] - ) - result = prompt.generate(template=template) + assert "secret.py" in expanded_result.prompt + assert "ignored.txt" in expanded_result.prompt + assert str(project / "src" / "main.py") in expanded_result.files + - # Check that custom template was used - assert "# Code Overview" in result.prompt - assert "## " in result.prompt +def test_line_numbers_and_code_block_control(project): + with_blocks = generate( + project, + include_patterns=["src/main.py"], + line_numbers=True, + ) + assert " 1 | def main():" in with_blocks.prompt + assert "```py" in with_blocks.prompt + without_blocks = generate( + project, + include_patterns=["src/main.py"], + no_codeblock=True, + ) + assert "def main():" in without_blocks.prompt + assert "```py" not in without_blocks.prompt -def test_token_count(test_dir): - """Test token counting.""" - prompt = Code2Prompt(path=test_dir) - # Get token count directly - token_count = prompt.token_count(encoding="cl100k") - assert isinstance(token_count, int) - assert token_count > 0 +def test_custom_handlebars_template_and_variables(project): + result = generate( + project, + include_patterns=["src/main.py"], + template_name="custom", + template_str="Audience={{audience}};{{#each files}}{{path}}={{code}}{{/each}}", + user_variables={"audience": "maintainers"}, + ) + assert result.prompt.startswith("Audience=maintainers;") + assert "src/main.py=def main():" in result.prompt - # Compare with generated result - result = prompt.generate(encoding="cl100k") - assert result.token_count == token_count -def test_multiple_encoding_options(test_dir): - """Test with different encoding options.""" - prompt = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"] +def test_xml_and_json_output_formats(project): + xml_result = generate( + project, + include_patterns=["src/main.py"], + output_format=OutputFormat.Xml, ) + assert "" in xml_result.prompt + assert '' in xml_result.prompt - # Try different encodings - encodings = ["cl100k", "gpt2", "p50k_base"] - token_counts = {} - - for encoding in encodings: - try: - count = prompt.token_count(encoding=encoding) - token_counts[encoding] = count - except Exception as e: - # Some encodings might not be available, that's OK - print(f"Encoding {encoding} failed: {e}") - - # At least one encoding should work - assert len(token_counts) > 0 - - # Different encodings might give different counts - # (but for very small files they might be the same) - if len(token_counts) > 1: - unique_counts = set(token_counts.values()) - print(f"Token counts: {token_counts}") \ No newline at end of file + json_result = generate( + project, + include_patterns=["src/main.py"], + output_format=OutputFormat.Json, + ) + document = json.loads(json_result.prompt) + assert document["files"] == ["src/main.py"] + assert document["token_count"] == json_result.token_count + + +def test_all_tokenizers_generate_counts(project): + counts = [ + generate( + project, + include_patterns=["src/main.py"], + encoding=tokenizer, + ).token_count + for tokenizer in ( + TokenizerType.Cl100kBase, + TokenizerType.O200kBase, + TokenizerType.P50kBase, + TokenizerType.P50kEdit, + TokenizerType.R50kBase, + ) + ] + assert all(count > 0 for count in counts) diff --git a/crates/code2prompt-python/tests/test_git.py b/crates/code2prompt-python/tests/test_git.py new file mode 100644 index 00000000..2e5ad583 --- /dev/null +++ b/crates/code2prompt-python/tests/test_git.py @@ -0,0 +1,36 @@ +"""Tests for Git operations delegated to Code2PromptSession.""" + +from code2prompt_rs import Code2PromptConfig, Code2PromptSession + +from .conftest import run_git + + +def test_branch_diff_log_and_tree_pruning(git_project): + session = Code2PromptSession( + Code2PromptConfig( + str(git_project), + diff_branches=("main", "feature"), + log_branches=("main", "feature"), + ) + ) + + session.load_codebase() + session.load_git_diff_between_branches() + session.load_git_log_between_branches() + + data = session.data + assert "changed.py" in data.source_tree + assert "unchanged.py" not in data.source_tree + assert [file.path for file in data.files] == ["changed.py"] + assert "VALUE = 1" in data.git_diff_branch + assert "VALUE = 2" in data.git_diff_branch + assert "change value" in data.git_log_branch + + +def test_staged_git_diff(git_project): + (git_project / "changed.py").write_text("VALUE = 3\n", encoding="utf-8") + run_git(git_project, "add", "changed.py") + + session = Code2PromptSession(Code2PromptConfig(str(git_project))) + session.load_git_diff() + assert "VALUE = 3" in session.data.git_diff diff --git a/crates/code2prompt-python/tests/test_processors.py b/crates/code2prompt-python/tests/test_processors.py new file mode 100644 index 00000000..ea9139e2 --- /dev/null +++ b/crates/code2prompt-python/tests/test_processors.py @@ -0,0 +1,31 @@ +"""Tests for configurable file processors exposed through the core config.""" + +from code2prompt_rs import ( + Code2PromptConfig, + Code2PromptSession, + FileProcessorsConfig, + IpynbProcessorConfig, +) + + +def test_notebook_processor_configuration_reaches_core(project): + processors = FileProcessorsConfig( + ipynb=IpynbProcessorConfig( + max_code_cells=1, + include_outputs=True, + include_markdown=True, + ) + ) + session = Code2PromptSession( + Code2PromptConfig( + str(project), + include_patterns=["notebook.ipynb"], + processors=processors, + ) + ) + + result = session.generate_prompt() + assert "Markdown Cell #1:" in result.prompt + assert "Code Cell #1:" in result.prompt + assert "Output:\nhello" in result.prompt + assert "[1 more code cells omitted]" in result.prompt diff --git a/crates/code2prompt-python/tests/test_session.py b/crates/code2prompt-python/tests/test_session.py new file mode 100644 index 00000000..0cc925c9 --- /dev/null +++ b/crates/code2prompt-python/tests/test_session.py @@ -0,0 +1,73 @@ +"""Tests for the stateful selection API exposed by Code2PromptSession.""" + +from pathlib import Path + +from code2prompt_rs import Code2PromptConfig, Code2PromptSession, SessionData + + +def test_select_deselect_and_toggle_use_the_same_session(project): + session = Code2PromptSession( + Code2PromptConfig(str(project), exclude_patterns=["**/*"]) + ) + + assert session.is_file_selected("src/main.py") is False + assert session.get_selected_files() == [] + + assert session.select_file("src/main.py") is session + assert session.has_user_actions() is True + assert session.is_file_selected("src/main.py") is True + assert session.get_selected_files() == [Path("src/main.py")] + + assert session.toggle_file_selection("src/main.py") is session + assert session.is_file_selected("src/main.py") is False + assert session.deselect_file("src/utils.py") is session + + +def test_absolute_selection_is_normalized_to_relative_path(project): + session = Code2PromptSession(Code2PromptConfig(str(project), deselected=True)) + absolute = str(project / "src" / "utils.py") + + session.select_file(absolute) + assert session.is_file_selected(absolute) is True + assert session.is_file_selected("src/utils.py") is True + assert session.get_selected_files() == [Path("src/utils.py")] + + +def test_clear_actions_restores_pattern_selection(project): + session = Code2PromptSession( + Code2PromptConfig(str(project), exclude_patterns=["**/*"]) + ) + session.select_file("src/main.py") + assert session.clear_user_actions() is session + assert session.has_user_actions() is False + assert session.get_selected_files() == [] + + +def test_pattern_and_deselected_mutators_update_core_config(project): + session = Code2PromptSession(Code2PromptConfig(str(project))) + + assert session.add_include_pattern("**/*.py") is session + assert session.add_exclude_pattern("**/tests/**") is session + assert session.set_deselected(True) is session + + assert session.config.include_patterns == ["**/*.py"] + assert session.config.exclude_patterns == ["**/tests/**"] + assert session.config.deselected is True + + +def test_loaded_session_data_contains_typed_file_entries(project): + session = Code2PromptSession( + Code2PromptConfig(str(project), include_patterns=["src/main.py"]) + ) + assert session.raw_analysis() is None + + session.load_codebase() + data = session.data + assert isinstance(data, SessionData) + assert data.absolute_code_path == project.name + assert data.source_tree is not None + assert data.files is not None + assert len(data.files) == 1 + assert data.files[0].path == "src/main.py" + assert data.files[0].extension == "py" + assert data.files[0].metadata.is_dir is False diff --git a/crates/code2prompt-python/tests/test_special_feature.py b/crates/code2prompt-python/tests/test_special_feature.py deleted file mode 100644 index 96142ebf..00000000 --- a/crates/code2prompt-python/tests/test_special_feature.py +++ /dev/null @@ -1,77 +0,0 @@ -## test_special_features.py - Tests pour fonctionnalités spéciales - -"""Tests for special features of Code2Prompt.""" -import pytest -import os -from pathlib import Path -from code2prompt_rs import Code2Prompt - -def test_hidden_files(test_dir): - """Test handling of hidden files.""" - # First, with hidden files excluded (default) - prompt = Code2Prompt(path=test_dir) - result = prompt.generate() - - # The .secret directory should be excluded - assert "secret.txt" not in result.prompt - - # Now, include hidden files - prompt = Code2Prompt( - path=test_dir, - include_hidden=True - ) - result = prompt.generate() - - # Should include .secret directory now - assert "secret.txt" in result.prompt or ".secret/secret.txt" in result.prompt - -def test_directory_tree(test_dir): - """Test full directory tree generation.""" - prompt = Code2Prompt( - path=test_dir, - full_directory_tree=True - ) - result = prompt.generate() - - # Should include directory structure - assert "lowercase" in result.prompt - assert "uppercase" in result.prompt - -def test_no_code_blocks(test_dir): - """Test generation without code blocks.""" - # With code blocks (default) - prompt = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"] - ) - with_blocks = prompt.generate() - - # Without code blocks - prompt = Code2Prompt( - path=test_dir, - include_patterns=["lowercase/foo.py"], - code_blocks=False - ) - without_blocks = prompt.generate() - - # Code blocks typically include ```python or ```py - assert "```py" in with_blocks.prompt - assert "```py" not in without_blocks.prompt - -def test_sort_files(test_dir): - """Test different sorting methods if available.""" - # This test depends on if sort_by is exposed in your API - try: - # Default should be name ascending - prompt = Code2Prompt(path=test_dir) - session = prompt.session() - - # Try to sort by name_desc if method exists - if hasattr(session, "sort_by"): - session = session.sort_by("name_desc") - result = session.generate() - # Hard to verify sort in output, but should not error - assert result is not None - except AttributeError: - # If sort_by isn't implemented, just pass the test - pass \ No newline at end of file diff --git a/crates/code2prompt-python/uv.lock b/crates/code2prompt-python/uv.lock new file mode 100644 index 00000000..9e280fc6 --- /dev/null +++ b/crates/code2prompt-python/uv.lock @@ -0,0 +1,104 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "code2prompt-rs" +version = "4.3.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "maturin" }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "maturin", specifier = ">=1.8.2,<2.0" }, + { name = "pytest", specifier = ">=8.3.5" }, +] + +[[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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "maturin" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, + { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, + { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] diff --git a/website/src/content/docs/de/docs/how_to/install.mdx b/website/src/content/docs/de/docs/how_to/install.mdx index 1ccd3d67..9c53f0f7 100644 --- a/website/src/content/docs/de/docs/how_to/install.mdx +++ b/website/src/content/docs/de/docs/how_to/install.mdx @@ -153,7 +153,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) und Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 Repository klonen : @@ -164,18 +165,18 @@ pip install code2prompt_rs 3. 📦 Abhängigkeiten installieren : - Der `rye`-Befehl erstellt eine virtuelle Umgebung und installiert alle Abhängigkeiten. + `uv` erstellt `.venv`, sperrt die Entwicklungsabhängigkeiten und baut die native Erweiterung im editierbaren Modus. ```sh - rye sync + uv sync ``` -4. ⚙️ Paket bauen : +4. 🧪 Tests ausführen : - Sie werden das Paket in der virtuellen Umgebung im `.venv`-Verzeichnis entwickeln. + Führen Sie den Testbefehl des Projekts in der synchronisierten Umgebung aus. ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/de/docs/tutorials/getting_started.mdx b/website/src/content/docs/de/docs/tutorials/getting_started.mdx index 338c64ca..71c2b55f 100644 --- a/website/src/content/docs/de/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/de/docs/tutorials/getting_started.mdx @@ -77,17 +77,17 @@ Siehe die Tutorials [Lernen Sie Kontextfilterung](/../docs/tutorials/learn_filte Für die programmgesteuerte Kontrolle verwenden Sie das Python-SDK: ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` Dies erfordert die Installation des SDK (`pip install code2prompt_rs`). Lesen Sie die SDK-Dokumentation für weitere Details. diff --git a/website/src/content/docs/docs/how_to/install.mdx b/website/src/content/docs/docs/how_to/install.mdx index f96f7807..fc36f221 100644 --- a/website/src/content/docs/docs/how_to/install.mdx +++ b/website/src/content/docs/docs/how_to/install.mdx @@ -153,7 +153,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) and Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 Clone the repository : @@ -164,18 +165,18 @@ pip install code2prompt_rs 3. 📦 Install the dependencies : - The `rye` command will create a virtual environment and install all the dependencies. + `uv` creates `.venv`, locks the development dependencies, and builds the native extension in editable mode. ```sh - rye sync + uv sync ``` -4. ⚙️ Build the package : +4. 🧪 Run the tests : - You will develop the package in the virtual environment located in `.venv` folder at the root of the project. + Run the project's test command in the synchronized environment. ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/docs/tutorials/getting_started.mdx b/website/src/content/docs/docs/tutorials/getting_started.mdx index 7072e657..b9716ef3 100644 --- a/website/src/content/docs/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/docs/tutorials/getting_started.mdx @@ -100,17 +100,17 @@ To learn more about `code2prompt`, check out the following tutorials: For programmatic control, use the Python SDK: ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` This requires installing the SDK (`pip install code2prompt_rs`). Refer to the SDK documentation for more details. @@ -118,4 +118,3 @@ This requires installing the SDK (`pip install code2prompt_rs`). Refer to the SD ## 🤖 Harness Integration To reduce token usage of your harness, you can let your agent run `code2prompt` as a MCP server or as a skill. This allows agents to request code context dynamically in a way that replaces multiple `find`, `glob` and `grep` calls. - diff --git a/website/src/content/docs/es/docs/how_to/install.mdx b/website/src/content/docs/es/docs/how_to/install.mdx index e2339069..45d4624f 100644 --- a/website/src/content/docs/es/docs/how_to/install.mdx +++ b/website/src/content/docs/es/docs/how_to/install.mdx @@ -153,7 +153,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) y Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 Clonar el repositorio : @@ -164,18 +165,18 @@ pip install code2prompt_rs 3. 📦 Instalar dependencias : - El comando `rye` creará un entorno virtual e instalará todas las dependencias. + `uv` crea `.venv`, bloquea las dependencias de desarrollo y compila la extensión nativa en modo editable. ```sh - rye sync + uv sync ``` -4. ⚙️ Compilar el paquete : +4. 🧪 Ejecutar las pruebas : - Desarrollará el paquete en el entorno virtual ubicado en la carpeta `.venv` en la raíz del proyecto. + Ejecute el comando de pruebas del proyecto en el entorno sincronizado. ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/es/docs/tutorials/getting_started.mdx b/website/src/content/docs/es/docs/tutorials/getting_started.mdx index ef489583..d4aab518 100644 --- a/website/src/content/docs/es/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/es/docs/tutorials/getting_started.mdx @@ -76,17 +76,17 @@ Consulte los tutoriales [Aprender filtrado de contexto ](/../docs/tutorials/lear Para obtener control programático, utilice el SDK de Python: ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` Esto requiere instalar el SDK (`pip install code2prompt_rs`). Consulte la documentación del SDK para obtener más detalles. diff --git a/website/src/content/docs/fr/docs/how_to/install.mdx b/website/src/content/docs/fr/docs/how_to/install.mdx index 83a92a97..cffb4945 100644 --- a/website/src/content/docs/fr/docs/how_to/install.mdx +++ b/website/src/content/docs/fr/docs/how_to/install.mdx @@ -152,7 +152,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) et Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 Cloner le référentiel : @@ -163,18 +164,18 @@ pip install code2prompt_rs 3. 📦 Installer les dépendances : - La commande `rye` créera un environnement virtuel et installera toutes les dépendances. + `uv` crée `.venv`, verrouille les dépendances de développement et compile l'extension native en mode éditable. ```sh - rye sync + uv sync ``` -4. ⚙️ Construire le paquet : +4. 🧪 Exécuter les tests : - Vous développerez le paquet dans l'environnement virtuel situé dans le dossier `.venv` à la racine du projet. + Exécutez la commande de test du projet dans l'environnement synchronisé. ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/fr/docs/tutorials/getting_started.mdx b/website/src/content/docs/fr/docs/tutorials/getting_started.mdx index 464f07b5..e2350b84 100644 --- a/website/src/content/docs/fr/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/fr/docs/tutorials/getting_started.mdx @@ -78,17 +78,17 @@ Voir les tutoriels [Apprendre le filtrage de contexte](/../../docs/tutorials/lea Pour un contrôle programmatique, utilisez le SDK Python : ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` Cela nécessite l'installation du SDK (`pip install code2prompt_rs`). Référez-vous à la documentation du SDK pour plus de détails. diff --git a/website/src/content/docs/ja/docs/how_to/install.mdx b/website/src/content/docs/ja/docs/how_to/install.mdx index c4a16a51..99e5a7ce 100644 --- a/website/src/content/docs/ja/docs/how_to/install.mdx +++ b/website/src/content/docs/ja/docs/how_to/install.mdx @@ -151,7 +151,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install)とCargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 リポジトリをクローンする : @@ -162,18 +163,18 @@ pip install code2prompt_rs 3. 📦 依存関係をインストールする : - `rye`コマンドは、仮想環境を作成し、すべての依存関係をインストールします。 + `uv`は`.venv`を作成し、開発用依存関係をロックして、ネイティブ拡張を編集可能モードでビルドします。 ```sh - rye sync + uv sync ``` -4. ⚙️ パッケージをビルドする : +4. 🧪 テストを実行する : - プロジェクトのルートにある`.venv`フォルダ内の仮想環境でパッケージを開発します。 + 同期済みの環境でプロジェクトのテストコマンドを実行します。 ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/ja/docs/tutorials/getting_started.mdx b/website/src/content/docs/ja/docs/tutorials/getting_started.mdx index 39e2b1c6..827826cf 100644 --- a/website/src/content/docs/ja/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/ja/docs/tutorials/getting_started.mdx @@ -67,17 +67,17 @@ code2prompt my_project プログラム制御を使用するには、Python SDKを使用します。 ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` これには、SDK(`pip install code2prompt_rs`)のインストールが必要です。詳細については、SDKのドキュメントを参照してください。 diff --git a/website/src/content/docs/ru/docs/how_to/install.mdx b/website/src/content/docs/ru/docs/how_to/install.mdx index 00b79425..7b3c88de 100644 --- a/website/src/content/docs/ru/docs/how_to/install.mdx +++ b/website/src/content/docs/ru/docs/how_to/install.mdx @@ -152,7 +152,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) и Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 Клонирование репозитория : @@ -163,18 +164,18 @@ pip install code2prompt_rs 3. 📦 Установка зависимостей : - Команда `rye` создаст виртуальную среду и установит все зависимости. + `uv` создаёт `.venv`, фиксирует зависимости для разработки и собирает нативное расширение в редактируемом режиме. ```sh - rye sync + uv sync ``` -4. ⚙️ Сборка пакета : +4. 🧪 Запустить тесты : - Вы будете разрабатывать пакет в виртуальной среде, расположенной в папке `.venv` в корне проекта. + Запустите команду тестирования проекта в синхронизированной среде. ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/ru/docs/tutorials/getting_started.mdx b/website/src/content/docs/ru/docs/tutorials/getting_started.mdx index 1b74292b..713e5f9c 100644 --- a/website/src/content/docs/ru/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/ru/docs/tutorials/getting_started.mdx @@ -81,17 +81,17 @@ code2prompt my_project Для программного управления используйте Python SDK: ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` Это требует установки SDK (`pip install code2prompt_rs`). diff --git a/website/src/content/docs/zh/docs/how_to/install.mdx b/website/src/content/docs/zh/docs/how_to/install.mdx index eaac170c..5a21a0c6 100644 --- a/website/src/content/docs/zh/docs/how_to/install.mdx +++ b/website/src/content/docs/zh/docs/how_to/install.mdx @@ -151,7 +151,8 @@ pip install code2prompt_rs - [Rust](https://www.rust-lang.org/tools/install) 和 Cargo - [Git](https://git-scm.com/downloads) - - [Rye](https://rye.astral.sh/) + - [Python 3.11+](https://www.python.org/downloads/) + - [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. 📥 克隆仓库: @@ -162,18 +163,18 @@ pip install code2prompt_rs 3. 📦 安装依赖项: - `rye` 命令将创建虚拟环境并安装所有依赖项。 + `uv` 会创建 `.venv`、锁定开发依赖项,并以可编辑模式构建原生扩展。 ```sh - rye sync + uv sync ``` -4. ⚙️ 构建包: +4. 🧪 运行测试: - 您将在项目根目录的 `.venv` 文件夹中位于虚拟环境中开发包。 + 在同步后的环境中运行项目测试命令。 ```sh - rye run maturin develop -r + uv run pytest ``` diff --git a/website/src/content/docs/zh/docs/tutorials/getting_started.mdx b/website/src/content/docs/zh/docs/tutorials/getting_started.mdx index 743ac7e8..27daa147 100644 --- a/website/src/content/docs/zh/docs/tutorials/getting_started.mdx +++ b/website/src/content/docs/zh/docs/tutorials/getting_started.mdx @@ -66,17 +66,17 @@ code2prompt my_project 对于程序化控制,请使用Python SDK: ```python -from code2prompt_rs import Code2Prompt +from code2prompt_rs import Code2PromptConfig, Code2PromptSession -config = { - "path": "my_project", - "include_patterns": ["*.rs"], - "exclude_patterns": ["tests/*"], -} +config = Code2PromptConfig( + "my_project", + include_patterns=["*.rs"], + exclude_patterns=["tests/*"], +) -c2p = Code2Prompt(**config) -prompt = c2p.generate_prompt() -print(prompt) +session = Code2PromptSession(config) +result = session.generate_prompt() +print(result.prompt) ``` 这需要安装SDK(`pip install code2prompt_rs`)。有关更多详细信息,请参阅SDK文档。