diff --git a/.gitignore b/.gitignore index 2bda8bc..aaf2eef 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .vscode/* +.claude/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3684172..1be88bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,110 +1,24 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + - id: check-toml - id: check-added-large-files - id: check-merge-conflict - - id: check-json - - id: check-toml - - id: check-xml - id: debug-statements - - id: check-docstring-first - - id: requirements-txt-fixer - - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - language_version: python3 - args: [--line-length=88] - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.284 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.14 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 - hooks: - - id: mypy - additional_dependencies: [types-all] - args: [--ignore-missing-imports] - - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [--profile=black, --line-length=88] - - - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 - hooks: - - id: pyupgrade - args: [--py38-plus] - - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.2 - hooks: - - id: prettier - types_or: [yaml, markdown, json] - - - repo: https://github.com/adamchainz/django-upgrade - rev: 1.12.0 - hooks: - - id: django-upgrade - args: [--target-version, "4.2"] - - - repo: https://github.com/asottile/yesqa - rev: v1.4.0 - hooks: - - id: yesqa - additional_dependencies: [flake8] - - - repo: https://github.com/pre-commit/mirrors-bandit - rev: v1.7.5 - hooks: - - id: bandit - args: [-r, src/, -f, json, -o, bandit-report.json] - exclude: tests/ - - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.4.1 hooks: - id: codespell - args: [--skip=*.pyc,*.pyo,*.pyd,__pycache__,.git,.tox,.venv,venv,env,.mypy_cache,.pytest_cache,*.egg-info,dist,build] - - - repo: https://github.com/commitizen-tools/commitizen - rev: v3.13.0 - hooks: - - id: commitizen - - - repo: local - hooks: - - id: pytest-check - name: pytest-check - entry: pytest - language: system - pass_filenames: false - always_run: true - args: [--maxfail=1, --tb=short] - - - id: docs-check - name: docs-check - entry: bash - language: system - pass_filenames: false - always_run: true - args: [-c, "cd docs && make html"] - - - id: security-check - name: security-check - entry: safety - language: system - pass_filenames: false - always_run: true - args: [check, --json, --output, safety-report.json] + args: [--skip, "*.lock,*.pyc"] diff --git a/Makefile b/Makefile index def88a6..93c9197 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,34 @@ -.PHONY: help install install-dev test test-cov lint format type-check docs clean build release +.PHONY: help install install-dev test test-cov test-fast lint format type-check check docs docs-serve docs-clean clean build build-check release dev-setup ci demo validate show help: ## Show this help message - @echo "Available commands:" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' install: ## Install package in production mode - uv pip install -e . + uv sync --no-dev install-dev: ## Install package in development mode with all dependencies - uv pip install -e ".[dev,test,docs]" + uv sync pre-commit install test: ## Run tests - pytest + uv run pytest test-cov: ## Run tests with coverage - pytest --cov=depcon --cov-report=html --cov-report=term-missing + uv run pytest --cov=depcon --cov-report=html --cov-report=term-missing test-fast: ## Run tests without coverage (faster) - pytest --no-cov + uv run pytest --no-cov -lint: ## Run linting - ruff check src/ tests/ - black --check src/ tests/ +lint: ## Check code style + uv run ruff check src/ tests/ + uv run ruff format --check src/ tests/ format: ## Format code - black src/ tests/ - ruff check src/ tests/ --fix + uv run ruff format src/ tests/ + uv run ruff check --fix src/ tests/ type-check: ## Run type checking - mypy src/ + uv run ty check check: lint type-check test ## Run all checks @@ -43,81 +42,40 @@ docs-clean: ## Clean documentation build rm -rf site/ clean: ## Clean build artifacts - rm -rf build/ - rm -rf dist/ - rm -rf *.egg-info/ - rm -rf .pytest_cache/ - rm -rf .coverage - rm -rf htmlcov/ - rm -rf .mypy_cache/ - rm -rf .ruff_cache/ + rm -rf build/ dist/ *.egg-info/ .pytest_cache/ .coverage htmlcov/ .mypy_cache/ .ruff_cache/ find . -type d -name __pycache__ -exec rm -rf {} + find . -type f -name "*.pyc" -delete build: ## Build package uv build -build-check: ## Check built package +build-check: ## Build and verify package uv build uv pip install dist/*.whl depcon --version -release: ## Release package (requires proper version bumping) - @echo "Make sure you have:" - @echo "1. Updated version in pyproject.toml" - @echo "2. Updated changelog" - @echo "3. Committed all changes" - @echo "4. Created and pushed tag" - @echo "Then run: uv publish" +release: ## Show release instructions + @echo "Release steps:" + @echo "1. Update version in pyproject.toml" + @echo "2. Update changelog" + @echo "3. Commit all changes" + @echo "4. Create and push tag: git tag v && git push --tags" + @echo "5. GitHub Actions will publish to PyPI" dev-setup: install-dev ## Set up development environment - @echo "Development environment set up!" - @echo "Run 'make check' to verify everything is working" + @echo "Development environment ready! Run 'make check' to verify." ci: ## Run CI checks locally - pre-commit run --all-files - pytest --cov=depcon --cov-report=xml - -security: ## Run security checks - safety check - bandit -r src/ - -benchmark: ## Run performance benchmarks - pytest --benchmark-only - -profile: ## Profile the code - python -m cProfile -o profile.stats -m depcon.cli --help - python -c "import pstats; pstats.Stats('profile.stats').sort_stats('cumulative').print_stats(20)" - -update-deps: ## Update dependencies - uv pip install --upgrade -e ".[dev,test,docs]" - -check-deps: ## Check for outdated dependencies - uv pip list --outdated - -install-hooks: ## Install pre-commit hooks - pre-commit install - -update-hooks: ## Update pre-commit hooks - pre-commit autoupdate - -run-hooks: ## Run pre-commit hooks on all files - pre-commit run --all-files + uv run ruff check src/ tests/ + uv run ruff format --check src/ tests/ + uv run ty check + uv run pytest --cov=depcon --cov-report=xml demo: ## Run demo script - python examples/demo.py + uv run python examples/demo.py validate: ## Validate pyproject.toml - depcon validate + uv run depcon validate show: ## Show dependencies - depcon show - -convert-example: ## Convert example requirements - depcon convert -r examples/requirements.txt --verbose - -# Development shortcuts -dev: install-dev ## Alias for install-dev -check-all: check ## Alias for check -test-all: test-cov ## Alias for test-cov -format-all: format ## Alias for format + uv run depcon show diff --git a/README.md b/README.md index 315c330..b9dcc6b 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,27 @@ # depcon ```text - | - __| _ _ __ __ _ _ -/ | |/ |/ \_/ / \_/ |/ | + | + __| _ _ __ __ _ _ +/ | |/ |/ \_/ / \_/ |/ | \_/|_/|__/|__/ \___/\__/ | |_/ - /| - \| + /| + \| ``` `depcon` is a modern, fully-featured tool for converting legacy `requirements.txt` files to the standardized `pyproject.toml` format with full PEP 621 support. It provides intelligent dependency grouping, validation, and seamless integration with modern Python packaging tools like `uv`, `hatchling`, and `setuptools`. ## Features -- **Full PEP 621 & PEP 735 Support**: Complete support for modern Python packaging standards +- **Full PEP 621, PEP 635 & PEP 735 Support**: Complete support for modern Python packaging standards +- **PEP 639 License**: Generates modern `license = "MIT"` SPDX string format - **Intelligent Dependency Grouping**: Automatically categorizes dependencies into main, dev, test, and docs groups +- **PEP 735 include-group**: Dependency groups can reference other groups (e.g., dev includes test) - **Proper Dependency Types**: Correctly distinguishes between dependency-groups (PEP 735) and optional-dependencies (PEP 621 extras) - **Advanced Parsing**: Handles complex requirements files including pip-tools, editable installs, and URLs - **Validation**: Built-in dependency validation and error checking - **Multiple Build Backends**: Support for hatchling, setuptools, and poetry -- **Tool Integration**: Automatic configuration for uv, hatch, and other modern tools - **Rich CLI**: Beautiful command-line interface with progress indicators and summaries -- **Flexible Configuration**: Extensive options for customization and control - **Export & Sync**: Export dependencies to requirements.txt and sync between formats ## Installation @@ -96,7 +96,7 @@ The main command for converting requirements files to modern pyproject.toml form - `--project-name TEXT`: Project name (if creating new pyproject.toml) - `--project-version TEXT`: Project version (if creating new pyproject.toml) - `--project-description TEXT`: Project description (if creating new pyproject.toml) -- `--python-version TEXT`: Python version requirement (default: >=3.11) +- `--python-version TEXT`: Python version requirement (default: >=3.12) - `--use-optional-deps / --use-dependency-groups`: Use optional-dependencies (PEP 621 extras) instead of dependency-groups (PEP 735) - `--remove-duplicates / --keep-duplicates`: Remove duplicate dependencies across groups (default: remove) - `--strict / --no-strict`: Strict mode: fail on parsing errors instead of warning @@ -199,7 +199,7 @@ depcon convert \ --project-name "my-project" \ --project-description "A great Python project" \ --project-version "1.0.0" \ - --python-version ">=3.9" + --python-version ">=3.12" # Use different build backend depcon convert -r requirements.txt --build-backend setuptools @@ -272,7 +272,7 @@ depcon sync --dry-run ## Generated pyproject.toml Structure -The tool generates modern `pyproject.toml` files following PEP 621 and PEP 735 standards: +The tool generates modern `pyproject.toml` files following PEP 621, PEP 639, and PEP 735 standards: ```toml [build-system] @@ -283,7 +283,8 @@ build-backend = "hatchling.build" name = "my-project" version = "1.0.0" description = "A great Python project" -requires-python = ">=3.11" +requires-python = ">=3.12" +license = "MIT" dependencies = [ "requests>=2.25.0", "numpy>=1.20.0", @@ -296,8 +297,7 @@ security = [ [dependency-groups] dev = [ - "pytest>=7.0.0", - "black>=23.0.0", + {include-group = "test"}, "ruff>=0.1.0", ] test = [ @@ -308,16 +308,13 @@ docs = [ "sphinx>=5.0.0", "sphinx-rtd-theme>=1.0.0", ] - -[tool.hatch.build.targets.wheel] -packages = ["src"] ``` ### Understanding Dependency Types - **`dependencies`**: Core runtime dependencies required for the package - **`[project.optional-dependencies]`** (PEP 621): Installable extras (e.g., `pip install package[security]`) -- **`[dependency-groups]`** (PEP 735): Development dependencies for tools like `uv` (not installable extras) +- **`[dependency-groups]`** (PEP 735): Development dependencies for tools like `uv` (not installable extras). Supports `include-group` to compose groups. ## Supported File Formats @@ -396,11 +393,11 @@ Comprehensive documentation is available at [https://lancereinsmith.github.io/de - **[API Reference](https://lancereinsmith.github.io/depcon/api_reference)** - Detailed API documentation - **[Examples](https://lancereinsmith.github.io/depcon/examples)** - Real-world usage examples - **[Contributing](https://lancereinsmith.github.io/depcon/contributing)** - How to contribute to the project -- **[Chaneglog](https://lancereinsmith.github.io/depcon/changelog)** - Changelog +- **[Changelog](https://lancereinsmith.github.io/depcon/changelog)** - Changelog ## Contributing -Contributions are welcome! Please see our [Contributing Guide](docs/contributing.md), [Development Guide](docs/development.md), [Contributors](docs/contributors.md), and [Code of Conduct](docs/code_of_conduct.md) for details. +Contributions are welcome! Please see our [Contributing Guide](docs/contributing.md) for details. ### Quick Development Setup @@ -410,7 +407,7 @@ git clone https://github.com/lancereinsmith/depcon.git cd depcon # Install in development mode -uv pip install -e ".[dev]" +uv sync # Install pre-commit hooks pre-commit install diff --git a/docs/api_reference.md b/docs/api_reference.md index f2047b0..9cbc1b2 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -6,23 +6,30 @@ Manual reference for the depcon API. Source: [src/depcon/](https://github.com/la ## depcon.models -Data models for dependency conversion. +Data models for dependency conversion. [models.py](https://github.com/lancereinsmith/depcon/blob/master/src/depcon/models.py) ### DependencySpec -Represents a single dependency specification (name, version_specs, extras, url, path, editable, markers). -`to_string()`, `to_pep621_string()`. +Represents a single dependency specification (name, version_specs, extras, url, path, editable, markers). + +- `to_string()` — pip-compatible format (includes `-e` flags, local paths). +- `to_pep621_string()` — PEP 621 format for pyproject.toml (omits editable flags and local paths). ### DependencyGroup -Represents a group of dependencies (e.g. dev, test, docs). -`add_dependency()`, `remove_dependency()`. +Represents a group of dependencies (e.g. dev, test, docs). + +- `include_groups: list[str]` — PEP 735 `include-group` references (e.g., dev group includes test group). +- `add_dependency()`, `remove_dependency()`. ### ProjectConfig -Complete project configuration (name, version, dependencies, optional_dependencies, dependency_groups, build_system, etc.). -`add_dependency()`, `get_dependency_group()`, `create_dependency_group()`. +Complete project configuration (name, version, dependencies, optional_dependencies, dependency_groups, build_system, etc.). + +- `license: str | dict | None` — PEP 639 SPDX string (preferred) or legacy dict format. +- `requires_python` — defaults to `">=3.12"`. +- `add_dependency()`, `get_dependency_group()`, `create_dependency_group()`. ### ConversionOptions @@ -32,46 +39,64 @@ Options for conversion: input files, output path, backup, append, group names, r ## depcon.parsers -Parsing of requirements file formats. +Parsing of requirements file formats. [parsers.py](https://github.com/lancereinsmith/depcon/blob/master/src/depcon/parsers.py) ### RequirementsParser -Parser for `requirements.txt` and `requirements.in`. -`parse()` → `list[DependencySpec]`. +Parser for `requirements.txt` and `requirements.in`. +`parse()` -> `list[DependencySpec]`. ### parse_requirements_file -`parse_requirements_file(file_path: Path) -> list[DependencySpec]` +`parse_requirements_file(file_path: Path) -> list[DependencySpec]` Parse a requirements file and return dependencies. Chooses `RequirementsParser` or `PipToolsParser` by content. ### group_dependencies_by_type -`group_dependencies_by_type(dependencies: list[DependencySpec]) -> dict[str, list[DependencySpec]]` +`group_dependencies_by_type(dependencies: list[DependencySpec]) -> dict[str, list[DependencySpec]]` Group dependencies into main, dev, test, docs by package-name heuristics. --- ## depcon.generators -Creation and manipulation of pyproject.toml. +Creation and manipulation of pyproject.toml. [generators.py](https://github.com/lancereinsmith/depcon/blob/master/src/depcon/generators.py) +### parse_dep_string + +`parse_dep_string(dep_str: str) -> DependencySpec | None` +Parse a PEP 621 dependency string (e.g. `"click>=8.0"`) into a `DependencySpec`. Returns `None` on invalid input. + ### PyProjectGenerator -Generate pyproject.toml from `ProjectConfig`. -`generate_toml_content()` → dict; writes to file. +Generate pyproject.toml from `ProjectConfig`. + +- `generate_toml_content()` -> dict — builds the full TOML structure. +- `write_to_file(file_path, backup=True)` — writes TOML using `tomli_w`. +- PEP 639: license emitted as SPDX string. Old dict formats normalized. +- PEP 735: `include-group` entries emitted before regular dependencies. ### PyProjectUpdater -Update existing pyproject.toml with new dependencies. -`update_with_dependencies(main_deps, dev_deps, test_deps, docs_deps, use_dependency_groups)`. +Update existing pyproject.toml with new dependencies. + +- `load_config() -> ProjectConfig` — loads and parses an existing pyproject.toml (public API). +- `update_with_dependencies(main_deps, dev_deps, test_deps, docs_deps, use_dependency_groups)`. + +### DependencyMerger + +Merge dependencies from multiple sources with optional append mode. + +- `merge_dependencies(existing, new)` -> `list[DependencySpec]`. +- `merge_dependency_groups(existing, new)` -> `dict[str, DependencyGroup]`. --- ## depcon.cli -Command-line interface. +Command-line interface. [cli.py](https://github.com/lancereinsmith/depcon/blob/master/src/depcon/cli.py) | Command | Description | diff --git a/docs/changelog.md b/docs/changelog.md index 1a046b8..33ffde3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2026-02-24 + +### Added + +- `parse_dep_string()` public helper in `generators` module for parsing PEP 621 dependency strings into `DependencySpec` objects. +- `DependencyGroup.include_groups` field for PEP 735 `include-group` support. Dependency groups can now reference other groups (e.g., dev includes test). +- Full PEP 735 `include-group` round-trip: loading, generating, and writing `{include-group = "..."}` entries in `[dependency-groups]`. +- `test_generators.py` with 23 tests covering TOML generation, PEP 639 license handling, PEP 735 include-group support, file I/O, backup creation, config loading, and round-trip integrity. + +### Changed + +- **PEP 639 license format**: License is now emitted as a simple SPDX string (`license = "MIT"`) instead of the legacy table format (`[project.license] text = "MIT"`). Old-format files are normalized on load. +- **Default Python version**: Changed from `>=3.8` / `>=3.11` to `>=3.12` across models, CLI, and generated output. Python 3.11 and earlier are EOL. +- **Removed `tomli` dependency**: Since Python >=3.12 is required, the stdlib `tomllib` is always available for TOML reading. Only `tomli-w` is needed for writing. +- **Classifiers updated**: Removed Python 3.11 classifier (not supported), added Python 3.14. +- **`_load_existing_config()` renamed to `load_config()`**: Now a public method on `PyProjectUpdater` instead of a private one. +- **`to_pep621_string()` now differs from `to_string()`**: PEP 621 output omits editable flags (`-e`) and local paths which are not valid in pyproject.toml dependency specifiers. +- **Logging replaces `print()`**: Parsers and generators now use `logging` module instead of bare `print()` calls. +- **Makefile modernized**: `lint` uses `ruff check` + `ruff format --check` (was black). `format` uses `ruff format` (was black). `type-check` uses `ty check` (was mypy). All commands use `uv run` and `uv sync`. +- **Pre-commit config rewritten**: Removed black, isort, mypy, flake8, django-upgrade, pyupgrade, yesqa, prettier, commitizen, bandit, safety, and broken local hooks. Now uses only pre-commit-hooks v5, ruff v0.14, and codespell v2.4. +- **Authors format**: Changed from `[[project.authors]]` table to inline `authors = [{...}]` syntax. + +### Removed + +- Dead TOML library fallback chains in generators (tried `tomli_w` -> `toml` -> manual writer, and `tomllib` -> `tomli` -> `toml`). Since Python >=3.12, `tomllib` is always available. +- `_write_toml_manually()` method (~85 lines of dead code) from `PyProjectGenerator`. +- Automatic empty `[tool.uv]` table generation (uv reads `[dependency-groups]` natively). +- Automatic `[tool.hatch.build]` with hardcoded `packages = ["src"]` (wrong for non-src layouts). + ## [0.5.0] - 2026-01-24 ### Added @@ -18,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Version: single source of truth in pyproject.toml** — Version is read only from `pyproject.toml`. `_version.py` uses `importlib.metadata` when installed, or reads `pyproject.toml` from source. Removed hardcoded `__version__` from `__init__.py`. CLI and `depcon.__version__` both derive from this. -- **Documentation: Sphinx → MkDocs** — All docs converted from RST to Markdown. `docs` dependency group: Sphinx deps replaced with mkdocs, mkdocs-material, mkdocstrings, pymdown-extensions. `[tool.rumdl]` flavor set to `mkdocs`. Makefile: `docs` / `docs-serve` / `docs-clean` use mkdocs and `site/`. GitHub docs workflow: `mkdocs build`, publish `site/` to GitHub Pages. +- **Documentation: Sphinx -> MkDocs** — All docs converted from RST to Markdown. `docs` dependency group: Sphinx deps replaced with mkdocs, mkdocs-material, mkdocstrings, pymdown-extensions. `[tool.rumdl]` flavor set to `mkdocs`. Makefile: `docs` / `docs-serve` / `docs-clean` use mkdocs and `site/`. GitHub docs workflow: `mkdocs build`, publish `site/` to GitHub Pages. - Docs trimmed: redundancy, novice-oriented content, and marketing removed; Code of Conduct shortened. Release/contributing docs: version only in `pyproject.toml`. ### Fixed diff --git a/docs/contributing.md b/docs/contributing.md index 50eb989..ec6a410 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,7 +2,14 @@ ## Setup -Fork, clone, then: `uv pip install -e ".[dev]"` and `pre-commit install`. Run `pytest` and `pre-commit run --all-files` before pushing. +Fork, clone, then: + +```bash +uv sync +pre-commit install +``` + +Run `uv run pytest` and `pre-commit run --all-files` before pushing. ## Pull requests @@ -10,7 +17,7 @@ Fork, clone, then: `uv pip install -e ".[dev]"` and `pre-commit install`. Run `p 2. Ensure tests pass, lint is clean, docs updated. 3. Push, open a PR, link related issues. -**PR template:** Description; type (bug fix / feature / docs / …); confirm tests pass and docs/changelog updated if needed. +**PR template:** Description; type (bug fix / feature / docs / ...); confirm tests pass and docs/changelog updated if needed. ## Issues @@ -20,10 +27,10 @@ Fork, clone, then: `uv pip install -e ".[dev]"` and `pre-commit install`. Run `p ## Project layout -- **models** — Data structures and validation -- **parsers** — Requirements file parsing -- **generators** — pyproject.toml generation -- **cli** — Commands +- **models** -- Data structures and validation +- **parsers** -- Requirements file parsing +- **generators** -- pyproject.toml generation +- **cli** -- Commands ## Adding features @@ -47,7 +54,7 @@ We pledge to make participation in this project harassment-free for everyone, re **Expected:** Welcoming and inclusive language; respect for differing views; accepting constructive criticism; empathy. -**Unacceptable:** Sexualized language or imagery; trolling, insults, or personal/political attacks; harassment; publishing others’ private information without permission; other conduct inappropriate in a professional setting. +**Unacceptable:** Sexualized language or imagery; trolling, insults, or personal/political attacks; harassment; publishing others' private information without permission; other conduct inappropriate in a professional setting. ### Reporting diff --git a/docs/installation.md b/docs/installation.md index 358f27d..339235e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,26 +18,26 @@ pipx install depcon ```bash pip install depcon -# pip install depcon[dev] | depcon[all] ``` ## From source ```bash git clone https://github.com/lancereinsmith/depcon.git && cd depcon -uv pip install -e ".[dev]" -# or: pip install -e ".[dev]" +uv sync +# or: pip install -e . ``` -## Optional dependency groups +## Dependency groups -- **dev**: ruff, ty, pytest, pre-commit -- **test**: pytest-cov, pytest-mock +depcon uses PEP 735 dependency groups for development: + +- **dev**: ruff, ty, rumdl (includes test group) +- **test**: pytest, pytest-cov, pytest-mock - **docs**: mkdocs, mkdocs-material, mkdocstrings -- **all**: all of the above -Example: `uv tool install depcon[dev,docs]`. +Install all dev dependencies: `uv sync` (installs all default groups). ## Requirements -Python ≥3.12. `uv`, `pip`, or `pipx` for installation. +Python >=3.12. `uv`, `pip`, or `pipx` for installation. diff --git a/docs/user_guide.md b/docs/user_guide.md index f7840d8..55a89da 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -7,10 +7,10 @@ Reference for depcon commands and options. depcon provides several commands: - **convert** — Convert requirements files to pyproject.toml -- **show**** — Display dependencies from pyproject.toml +- **show** — Display dependencies from pyproject.toml - **validate** — Validate pyproject.toml dependencies -- **list**** — List all dependency groups -- **check**** — Check for common issues +- **list** — List all dependency groups +- **check** — Check for common issues - **export** — Export dependencies to requirements.txt - **diff** — Show differences between files - **sync** — Sync dependencies to requirements files @@ -58,7 +58,7 @@ depcon convert -r requirements.txt - `--project-name TEXT`: Project name (if creating new pyproject.toml) - `--project-version TEXT`: Project version (if creating new pyproject.toml) - `--project-description TEXT`: Project description (if creating new pyproject.toml) -- `--python-version TEXT`: Python version requirement (default: >=3.11) +- `--python-version TEXT`: Python version requirement (default: >=3.12) - `--use-optional-deps / --use-dependency-groups`: Use optional-dependencies (PEP 621 extras) instead of dependency-groups (PEP 735) - `--remove-duplicates / --keep-duplicates`: Remove duplicate dependencies across groups (default: remove) - `--strict / --no-strict`: Strict mode: fail on parsing errors instead of warning @@ -134,9 +134,18 @@ build-backend = "hatchling.build" Setuptools: `requires = ["setuptools>=61.0", "wheel"]`, `build-backend = "setuptools.build_meta"`. Poetry: `poetry-core`, `poetry.core.masonry.api`. +## Generated output + +depcon generates modern pyproject.toml files following current standards: + +- **PEP 621**: Project metadata in `[project]` +- **PEP 639**: License as SPDX string (`license = "MIT"`) +- **PEP 735**: Development dependencies in `[dependency-groups]` with `include-group` support +- **Default Python**: `>=3.12` + ## Tool integration -Output uses `[dependency-groups]` for uv. For hatch: `[tool.hatch.build.targets.wheel]` with `packages = ["src"]`. +Output uses `[dependency-groups]` (PEP 735) by default, which uv reads natively. ## Troubleshooting diff --git a/pyproject.toml b/pyproject.toml index cda46ee..b4a33eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,18 +4,12 @@ build-backend = "hatchling.build" [project] name = "depcon" -version = "0.5.1" +version = "0.6.0" description = "Convert legacy requirements files to modern pyproject.toml format with full PEP 621 support" readme = "README.md" requires-python = ">=3.12" -dependencies = [ - "click>=8.3.1", - "packaging>=26.0", - "pydantic>=2.12.5", - "rich>=14.3.1", - "tomli>=2.4.0", - "tomli-w>=1.2.0", -] +license = "MIT" +authors = [{name = "Lance Reinsmith", email = "info@k2rad.com"}] keywords = [ "requirements", "dependencies", @@ -31,19 +25,19 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] - -[[project.authors]] -name = "Lance Reinsmith" -email = "info@k2rad.com" - -[project.license] -text = "MIT" +dependencies = [ + "click>=8.3.1", + "packaging>=26.0", + "pydantic>=2.12.5", + "rich>=14.3.1", + "tomli-w>=1.2.0", +] [project.scripts] depcon = "depcon.cli:main" @@ -56,7 +50,7 @@ Repository = "https://github.com/lancereinsmith/depcon" [dependency-groups] dev = [ - { include-group = "test" }, + {include-group = "test"}, "ruff>=0.14.14", "ty>=0.0.13", "rumdl>=0.1.0", @@ -72,7 +66,6 @@ docs = [ [tool.uv] default-groups = ["dev", "test", "docs"] -# ——— Ruff: lint and format (replaces black, isort, flake8) ——— [tool.ruff] line-length = 88 target-version = "py312" @@ -85,22 +78,18 @@ ignore = ["E501"] quote-style = "double" indent-style = "space" -# ——— ty: type checking (replaces mypy, pyright) ——— [tool.ty.environment] python-version = "3.12" [tool.ty.src] exclude = ["tests"] -# ——— rumdl: markdown format (mkdocs flavor) ——— -# Global options live under [tool.rumdl]; [tool.rumdl.global] is for .rumdl.toml only. [tool.rumdl] flavor = "mkdocs" respect-gitignore = true disable = ["MD007", "MD013", "MD031", "MD033", "MD046"] exclude = [".git", ".github", "node_modules", "vendor", "dist", "build", "CHANGELOG.md", "LICENSE.md"] -# ——— Pytest (optional: remove [tool.pytest.ini_options] and the test group if you have no tests) ——— [tool.pytest.ini_options] testpaths = ["tests"] addopts = ["-v", "--tb=short", "--strict-markers", "--strict-config"] diff --git a/src/depcon/cli.py b/src/depcon/cli.py index 17c41cf..e3fd83c 100644 --- a/src/depcon/cli.py +++ b/src/depcon/cli.py @@ -100,7 +100,7 @@ def main(): @click.option( "--project-description", help="Project description (if creating new pyproject.toml)" ) -@click.option("--python-version", default=">=3.11", help="Python version requirement") +@click.option("--python-version", default=">=3.12", help="Python version requirement") @click.option( "--use-optional-deps/--use-dependency-groups", "use_optional_deps", @@ -212,7 +212,7 @@ def convert( # Set project metadata if provided if project_name or project_description: - config = updater._load_existing_config() + config = updater.load_config() if project_name: config.name = project_name if project_description: @@ -271,7 +271,7 @@ def show(pyproject_file: Path, output_format: str, group: str | None): """Show dependencies from pyproject.toml file.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() # Filter by group if specified if group: @@ -358,7 +358,7 @@ def validate(pyproject_file: Path, group: str | None, check_pypi: bool): """Validate pyproject.toml dependencies.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() errors = [] warnings = [] @@ -423,7 +423,7 @@ def list_groups(pyproject_file: Path): """List all dependency groups in pyproject.toml.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() table = Table(title="Dependency Groups") table.add_column("Type", style="cyan") @@ -469,7 +469,7 @@ def check(pyproject_file: Path, check_duplicates: bool, check_missing: bool): """Check pyproject.toml for common issues.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() issues = [] warnings = [] @@ -602,7 +602,7 @@ def export(pyproject_file: Path, output_file: Path, group: str, include_hashes: """Export dependencies from pyproject.toml to requirements.txt format.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() dependencies = [] if group == "main" or group == "all": @@ -667,7 +667,7 @@ def diff(pyproject_file: Path, requirements_file: Path | None, group: str | None rich_diff = None updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() # Get dependencies from pyproject.toml pyproject_deps = [] @@ -737,7 +737,7 @@ def sync(pyproject_file: Path, group: tuple, dry_run: bool): """Sync dependencies from pyproject.toml to requirements files.""" try: updater = PyProjectUpdater(pyproject_file, ConversionOptions()) - config = updater._load_existing_config() + config = updater.load_config() group_list = list(group) if group else ["main", "dev", "test", "docs"] diff --git a/src/depcon/generators.py b/src/depcon/generators.py index a5a10f9..5ea537a 100644 --- a/src/depcon/generators.py +++ b/src/depcon/generators.py @@ -2,12 +2,35 @@ from __future__ import annotations +import logging import shutil +import tomllib from pathlib import Path from typing import Any +import tomli_w +from packaging.requirements import Requirement + from .models import DependencyGroup, DependencySpec, ProjectConfig +logger = logging.getLogger(__name__) + + +def parse_dep_string(dep_str: str) -> DependencySpec | None: + """Parse a PEP 621 dependency string into a DependencySpec.""" + try: + req = Requirement(dep_str) + return DependencySpec( + name=req.name, + version_specs=[str(spec) for spec in req.specifier] + if req.specifier + else [], + extras=list(req.extras), + markers=str(req.marker) if req.marker else None, + ) + except Exception: + return None + class PyProjectGenerator: """Generate and manipulate pyproject.toml files.""" @@ -18,20 +41,19 @@ def __init__(self, config: ProjectConfig): def generate_toml_content(self) -> dict[str, Any]: """Generate TOML content from project configuration.""" - content = {} + content: dict[str, Any] = {} # Build system content["build-system"] = self.config.build_system # Project metadata - project = { + project: dict[str, Any] = { "name": self.config.name, "version": self.config.version, "description": self.config.description, "requires-python": self.config.requires_python, } - # Add optional fields if self.config.readme: project["readme"] = self.config.readme @@ -39,7 +61,13 @@ def generate_toml_content(self) -> dict[str, Any]: project["authors"] = self.config.authors if self.config.license: - project["license"] = self.config.license + # PEP 639: emit license as a simple SPDX string + if isinstance(self.config.license, dict): + project["license"] = self.config.license.get( + "text", self.config.license.get("file", "") + ) + else: + project["license"] = self.config.license if self.config.keywords: project["keywords"] = self.config.keywords @@ -56,7 +84,7 @@ def generate_toml_content(self) -> dict[str, Any]: dep.to_pep621_string() for dep in self.config.dependencies ] - # Optional dependencies (PEP 621 extras - installable features) + # Optional dependencies (PEP 621 extras) if self.config.optional_dependencies: project["optional-dependencies"] = {} for group_name, group in self.config.optional_dependencies.items(): @@ -66,24 +94,29 @@ def generate_toml_content(self) -> dict[str, Any]: content["project"] = project - # Dependency groups (PEP 735 - for uv, etc., not installable extras) + # Dependency groups (PEP 735) if self.config.dependency_groups: content["dependency-groups"] = {} for group_name, group in self.config.dependency_groups.items(): - content["dependency-groups"][group_name] = [ - dep.to_pep621_string() for dep in group.dependencies - ] - - # Tool configurations (do not emit deprecated tool.uv.dev-dependencies) + group_list: list[str | dict[str, str]] = [] + # Emit include-group entries first + for included in group.include_groups: + group_list.append({"include-group": included}) + # Then regular dependencies + for dep in group.dependencies: + group_list.append(dep.to_pep621_string()) + content["dependency-groups"][group_name] = group_list + + # Tool configurations — pass through, cleaning deprecated keys if self.config.tool_configs: - # Remove any deprecated uv dev-dependencies if present tool_configs = dict(self.config.tool_configs) + # Remove deprecated tool.uv.dev-dependencies if "uv" in tool_configs and isinstance(tool_configs["uv"], dict): tool_configs["uv"].pop("dev-dependencies", None) if not tool_configs["uv"]: - # keep empty table if user expects [tool.uv], else it would be dropped - tool_configs["uv"] = {} - content["tool"] = tool_configs + del tool_configs["uv"] + if tool_configs: + content["tool"] = tool_configs return content @@ -92,141 +125,27 @@ def write_to_file(self, file_path: Path, backup: bool = True) -> None: if file_path.exists() and backup: backup_path = file_path.with_suffix(".toml.backup") shutil.copy2(file_path, backup_path) - print(f"Backup created: {backup_path}") + logger.info("Backup created: %s", backup_path) content = self.generate_toml_content() - - # Write TOML content - try: - import tomli_w - - with open(file_path, "wb") as f: - tomli_w.dump(content, f) - except ImportError: - # Fallback to toml library - try: - import toml # type: ignore[import-untyped] - - with open(file_path, "w", encoding="utf-8") as f: - toml.dump(content, f) - except ImportError: - # Last resort: write basic TOML manually - self._write_toml_manually(content, file_path) - - def _write_toml_manually(self, content: dict[str, Any], file_path: Path) -> None: - """Write TOML content manually as a fallback.""" - lines = [] - - # Write build-system - if "build-system" in content: - lines.append("[build-system]") - for key, value in content["build-system"].items(): - if isinstance(value, list): - lines.append(f"{key} = {value}") - else: - lines.append(f'{key} = "{value}"') - lines.append("") - - # Write project section - if "project" in content: - lines.append("[project]") - project = content["project"] - - # Basic fields - for field in [ - "name", - "version", - "description", - "readme", - "requires-python", - ]: - if field in project: - value = project[field] - if isinstance(value, str): - lines.append(f'{field} = "{value}"') - else: - lines.append(f"{field} = {value}") - - # Dependencies - if "dependencies" in project and project["dependencies"]: - lines.append("dependencies = [") - for dep in project["dependencies"]: - lines.append(f' "{dep}",') - lines.append("]") - - # Optional dependencies - if "optional-dependencies" in project and project["optional-dependencies"]: - lines.append("") - lines.append("[project.optional-dependencies]") - for group_name, deps in project["optional-dependencies"].items(): - lines.append(f"{group_name} = [") - for dep in deps: - lines.append(f' "{dep}",') - lines.append("]") - - lines.append("") - - # Write tool sections - if "tool" in content: - for tool_name, tool_config in content["tool"].items(): - lines.append(f"[tool.{tool_name}]") - for key, value in tool_config.items(): - if isinstance(value, dict): - lines.append(f"[tool.{tool_name}.{key}]") - for sub_key, sub_value in value.items(): - if isinstance(sub_value, list): - lines.append(f"{sub_key} = {sub_value}") - else: - lines.append(f'{sub_key} = "{sub_value}"') - elif isinstance(value, list): - lines.append(f"{key} = {value}") - else: - lines.append(f'{key} = "{value}"') - lines.append("") - - # Write dependency-groups (PEP 735 - uv modern config) - if "dependency-groups" in content and content["dependency-groups"]: - lines.append("[dependency-groups]") - for group_name, deps in content["dependency-groups"].items(): - lines.append(f"{group_name} = [") - for dep in deps: - lines.append(f' "{dep}",') - lines.append("]") - lines.append("") - - # Write to file - with open(file_path, "w", encoding="utf-8") as f: - f.write("\n".join(lines)) + with open(file_path, "wb") as f: + tomli_w.dump(content, f) def merge_with_existing(self, file_path: Path) -> ProjectConfig: """Merge with existing pyproject.toml file.""" if not file_path.exists(): return self.config - try: - import tomli - - with open(file_path, "rb") as f: - existing_data = tomli.load(f) - except ImportError: - try: - import toml # type: ignore[import-untyped] - - with open(file_path, encoding="utf-8") as f: - existing_data = toml.load(f) - except ImportError: - # If no TOML library is available, return empty config - return self.config + with open(file_path, "rb") as f: + existing_data = tomllib.load(f) - # Merge project metadata if "project" in existing_data: project_data = existing_data["project"] - # Update basic fields if not set if not self.config.name or self.config.name == "project-name": self.config.name = project_data.get("name", self.config.name) - if not self.config.description or self.config.description == "": + if not self.config.description: self.config.description = project_data.get( "description", self.config.description ) @@ -236,18 +155,21 @@ def merge_with_existing(self, file_path: Path) -> ProjectConfig: if ( not self.config.requires_python - or self.config.requires_python == ">=3.8" + or self.config.requires_python == ">=3.12" ): self.config.requires_python = project_data.get( "requires-python", self.config.requires_python ) - # Merge other metadata if "authors" in project_data: self.config.authors = project_data["authors"] if "license" in project_data: - self.config.license = project_data["license"] + license_val = project_data["license"] + if isinstance(license_val, dict) and "text" in license_val: + self.config.license = license_val["text"] + else: + self.config.license = license_val if "keywords" in project_data: self.config.keywords = project_data["keywords"] @@ -258,7 +180,6 @@ def merge_with_existing(self, file_path: Path) -> ProjectConfig: if "urls" in project_data: self.config.urls = project_data["urls"] - # Merge tool configurations if "tool" in existing_data: self.config.tool_configs = existing_data["tool"] @@ -279,18 +200,9 @@ def merge_dependencies( if not self.append_mode: return new - # Create a map of existing dependencies by name existing_map = {dep.name: dep for dep in existing} - - # Merge new dependencies for new_dep in new: - if new_dep.name in existing_map: - # Update existing dependency - existing_map[new_dep.name] = new_dep - else: - # Add new dependency - existing_map[new_dep.name] = new_dep - + existing_map[new_dep.name] = new_dep return list(existing_map.values()) def merge_dependency_groups( @@ -300,20 +212,13 @@ def merge_dependency_groups( if not self.append_mode: return new - # Start with existing groups merged = existing.copy() - - # Merge new groups for group_name, new_group in new.items(): if group_name in merged: - # Merge dependencies within the group - existing_group = merged[group_name] for dep in new_group.dependencies: - existing_group.add_dependency(dep) + merged[group_name].add_dependency(dep) else: - # Add new group merged[group_name] = new_group - return merged @@ -344,216 +249,91 @@ def update_with_dependencies( use_dependency_groups: If True, use dependency-groups (PEP 735), otherwise use optional-dependencies (PEP 621 extras) """ - # Load existing configuration - config = self._load_existing_config() + config = self.load_config() - # Merge main dependencies if main_deps: config.dependencies = self.merger.merge_dependencies( config.dependencies, main_deps ) - # Merge development dependencies - if dev_deps: - dev_group = config.get_dependency_group( - self.options.dev_group_name, use_dependency_groups=use_dependency_groups - ) - if not dev_group: - dev_group = config.create_dependency_group( - self.options.dev_group_name, - "Development dependencies", - use_dependency_groups=use_dependency_groups, + for deps, group_name_attr in [ + (dev_deps, "dev_group_name"), + (test_deps, "test_group_name"), + (docs_deps, "docs_group_name"), + ]: + if deps: + name = getattr(self.options, group_name_attr) + group = config.get_dependency_group( + name, use_dependency_groups=use_dependency_groups ) - - existing_dev_deps = dev_group.dependencies - dev_group.dependencies = self.merger.merge_dependencies( - existing_dev_deps, dev_deps - ) - - # Merge test dependencies - if test_deps: - test_group = config.get_dependency_group( - self.options.test_group_name, - use_dependency_groups=use_dependency_groups, - ) - if not test_group: - test_group = config.create_dependency_group( - self.options.test_group_name, - "Test dependencies", - use_dependency_groups=use_dependency_groups, - ) - - existing_test_deps = test_group.dependencies - test_group.dependencies = self.merger.merge_dependencies( - existing_test_deps, test_deps - ) - - # Merge documentation dependencies - if docs_deps: - docs_group = config.get_dependency_group( - self.options.docs_group_name, - use_dependency_groups=use_dependency_groups, - ) - if not docs_group: - docs_group = config.create_dependency_group( - self.options.docs_group_name, - "Documentation dependencies", - use_dependency_groups=use_dependency_groups, + if not group: + group = config.create_dependency_group( + name, use_dependency_groups=use_dependency_groups + ) + group.dependencies = self.merger.merge_dependencies( + group.dependencies, deps ) - existing_docs_deps = docs_group.dependencies - docs_group.dependencies = self.merger.merge_dependencies( - existing_docs_deps, docs_deps - ) - - # Update tool configurations - self._update_tool_configs(config) - - # Write updated configuration generator = PyProjectGenerator(config) generator.write_to_file(self.file_path, backup=self.options.backup) - def _load_existing_config(self) -> ProjectConfig: - """Load existing project configuration.""" + def load_config(self) -> ProjectConfig: + """Load project configuration from pyproject.toml.""" if not self.file_path.exists(): return ProjectConfig(name="project-name") - data = None - - # Try different TOML libraries in order of preference - try: - # Python 3.11+ built-in tomllib - import tomllib - - with open(self.file_path, "rb") as f: - data = tomllib.load(f) - except ImportError: - try: - # tomli (faster, more modern) - import tomli - - with open(self.file_path, "rb") as f: - data = tomli.load(f) - except ImportError: - try: - # toml (older, more compatible) - import toml # type: ignore[import-untyped] - - with open(self.file_path, encoding="utf-8") as f: - data = toml.load(f) - except ImportError: - # If no TOML library is available, return empty config - return ProjectConfig(name="project-name") - - if data is None: - return ProjectConfig(name="project-name") + with open(self.file_path, "rb") as f: + data = tomllib.load(f) - # Extract project metadata project_data = data.get("project", {}) + # Normalize license to string (PEP 639) + license_val = project_data.get("license") + if isinstance(license_val, dict): + license_val = license_val.get("text", license_val.get("file")) + config = ProjectConfig( name=project_data.get("name", "project-name"), version=project_data.get("version", "0.1.0"), description=project_data.get("description", ""), readme=project_data.get("readme"), - requires_python=project_data.get("requires-python", ">=3.8"), + requires_python=project_data.get("requires-python", ">=3.12"), authors=project_data.get("authors", []), - license=project_data.get("license"), + license=license_val, keywords=project_data.get("keywords", []), classifiers=project_data.get("classifiers", []), urls=project_data.get("urls", {}), ) # Extract dependencies - if "dependencies" in project_data: - # Convert dependency strings back to DependencySpec objects - for dep_str in project_data["dependencies"]: - try: - from packaging.requirements import Requirement - - req = Requirement(dep_str) - dep = DependencySpec( - name=req.name, - version_specs=( - [str(spec) for spec in req.specifier] - if req.specifier - else [] - ), - extras=list(req.extras), - markers=str(req.marker) if req.marker else None, - ) - config.dependencies.append(dep) - except Exception: - # Skip invalid dependencies - continue + for dep_str in project_data.get("dependencies", []): + dep = parse_dep_string(dep_str) + if dep: + config.dependencies.append(dep) # Extract optional dependencies (PEP 621 extras) - if "optional-dependencies" in project_data: - for group_name, deps in project_data["optional-dependencies"].items(): - group = DependencyGroup(name=group_name) - for dep_str in deps: - try: - from packaging.requirements import Requirement - - req = Requirement(dep_str) - dep = DependencySpec( - name=req.name, - version_specs=( - [str(spec) for spec in req.specifier] - if req.specifier - else [] - ), - extras=list(req.extras), - markers=str(req.marker) if req.marker else None, - ) - group.add_dependency(dep) - except Exception: - # Skip invalid dependencies - continue - config.optional_dependencies[group_name] = group + for group_name, deps in project_data.get("optional-dependencies", {}).items(): + group = DependencyGroup(name=group_name) + for dep_str in deps: + dep = parse_dep_string(dep_str) + if dep: + group.add_dependency(dep) + config.optional_dependencies[group_name] = group # Extract dependency groups (PEP 735) - if "dependency-groups" in data: - for group_name, deps in data["dependency-groups"].items(): - group = DependencyGroup(name=group_name) - for dep_str in deps: - try: - from packaging.requirements import Requirement - - req = Requirement(dep_str) - dep = DependencySpec( - name=req.name, - version_specs=( - [str(spec) for spec in req.specifier] - if req.specifier - else [] - ), - extras=list(req.extras), - markers=str(req.marker) if req.marker else None, - ) + for group_name, items in data.get("dependency-groups", {}).items(): + group = DependencyGroup(name=group_name) + for item in items: + if isinstance(item, dict) and "include-group" in item: + group.include_groups.append(item["include-group"]) + elif isinstance(item, str): + dep = parse_dep_string(item) + if dep: group.add_dependency(dep) - except Exception: - # Skip invalid dependencies - continue - config.dependency_groups[group_name] = group + config.dependency_groups[group_name] = group # Extract tool configurations if "tool" in data: config.tool_configs = data["tool"] return config - - def _update_tool_configs(self, config: ProjectConfig) -> None: - """Update tool-specific configurations.""" - if self.options.enable_uv and "uv" not in config.tool_configs: - # No longer write deprecated tool.uv.dev-dependencies; uv reads dependency-groups - config.tool_configs["uv"] = {} - - if self.options.enable_hatch: - if "hatch" not in config.tool_configs: - config.tool_configs["hatch"] = {} - - # Add build configuration - config.tool_configs["hatch"]["build"] = { - "targets": {"wheel": {"packages": ["src"]}} - } diff --git a/src/depcon/models.py b/src/depcon/models.py index 95067cc..11104ad 100644 --- a/src/depcon/models.py +++ b/src/depcon/models.py @@ -59,8 +59,21 @@ def to_string(self) -> str: return "".join(parts) def to_pep621_string(self) -> str: - """Convert to PEP 621 compatible string format.""" - return self.to_string() + """Convert to PEP 621 compatible string format. + + Unlike to_string(), this omits editable flags and local paths + which are not valid in PEP 621 dependency specifiers. + """ + parts = [self.name] + if self.extras: + parts[0] += f"[{','.join(self.extras)}]" + if self.url: + parts.append(f" @ {self.url}") + elif self.version_specs: + parts.extend(self.version_specs) + if self.markers: + parts.append(f"; {self.markers}") + return "".join(parts) class DependencyGroup(BaseModel): @@ -70,6 +83,9 @@ class DependencyGroup(BaseModel): dependencies: list[DependencySpec] = Field( default_factory=list, description="Dependencies in this group" ) + include_groups: list[str] = Field( + default_factory=list, description="Included dependency groups (PEP 735)" + ) description: str | None = Field( None, description="Optional description of the group" ) @@ -107,12 +123,14 @@ class ProjectConfig(BaseModel): description: str = Field(default="", description="Project description") readme: str | None = Field(None, description="Path to README file") requires_python: str = Field( - default=">=3.8", description="Python version requirement" + default=">=3.12", description="Python version requirement" ) authors: list[dict[str, str]] = Field( default_factory=list, description="Project authors" ) - license: dict[str, str] | None = Field(None, description="Project license") + license: str | dict[str, str] | None = Field( + None, description="Project license (SPDX string per PEP 639)" + ) keywords: list[str] = Field(default_factory=list, description="Project keywords") classifiers: list[str] = Field( default_factory=list, description="Project classifiers" diff --git a/src/depcon/parsers.py b/src/depcon/parsers.py index b4238c7..abc9666 100644 --- a/src/depcon/parsers.py +++ b/src/depcon/parsers.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging import re from pathlib import Path @@ -9,6 +10,8 @@ from .models import DependencySpec +logger = logging.getLogger(__name__) + class RequirementsParser: """Parser for requirements.txt and requirements.in files.""" @@ -38,10 +41,13 @@ def parse(self) -> list[DependencySpec]: if dep: dependencies.append(dep) except Exception as e: - print( - f"Warning: Failed to parse line {line_num} in {self.file_path}: {line}" + logger.warning( + "Failed to parse line %d in %s: %s (%s)", + line_num, + self.file_path, + line, + e, ) - print(f"Error: {e}") continue return dependencies @@ -192,10 +198,13 @@ def parse(self) -> list[DependencySpec]: else: current_dep = None except Exception as e: - print( - f"Warning: Failed to parse line {line_num} in {self.file_path}: {line}" + logger.warning( + "Failed to parse line %d in %s: %s (%s)", + line_num, + self.file_path, + line, + e, ) - print(f"Error: {e}") current_dep = None continue diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000..11a8013 --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,292 @@ +"""Tests for the generators module.""" + +from __future__ import annotations + +import tomllib + +from depcon.generators import PyProjectGenerator, PyProjectUpdater, parse_dep_string +from depcon.models import ( + ConversionOptions, + DependencyGroup, + DependencySpec, + ProjectConfig, +) + + +class TestParseDepString: + """Tests for the parse_dep_string helper.""" + + def test_simple_package(self): + dep = parse_dep_string("requests") + assert dep is not None + assert dep.name == "requests" + assert dep.version_specs == [] + + def test_package_with_version(self): + dep = parse_dep_string("requests>=2.28.0") + assert dep is not None + assert dep.name == "requests" + assert ">=2.28.0" in dep.version_specs + + def test_package_with_extras(self): + dep = parse_dep_string("uvicorn[standard]>=0.20.0") + assert dep is not None + assert dep.name == "uvicorn" + assert "standard" in dep.extras + + def test_package_with_markers(self): + dep = parse_dep_string('pywin32; sys_platform == "win32"') + assert dep is not None + assert dep.name == "pywin32" + assert dep.markers is not None + assert "win32" in dep.markers + + def test_invalid_string_returns_none(self): + dep = parse_dep_string("not a valid!!!! spec @@@") + assert dep is None + + def test_url_dependency(self): + dep = parse_dep_string("mypackage @ https://example.com/pkg.tar.gz") + assert dep is not None + assert dep.name == "mypackage" + + +class TestPyProjectGenerator: + """Tests for PyProjectGenerator.""" + + def test_basic_generation(self): + config = ProjectConfig( + name="test-project", + version="1.0.0", + description="A test project", + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + assert content["build-system"]["requires"] == ["hatchling"] + assert content["project"]["name"] == "test-project" + assert content["project"]["version"] == "1.0.0" + + def test_pep639_license_from_string(self): + config = ProjectConfig(name="test", license="MIT") + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + assert content["project"]["license"] == "MIT" + + def test_pep639_license_from_dict(self): + config = ProjectConfig(name="test", license={"text": "Apache-2.0"}) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + # Should normalize dict to string + assert content["project"]["license"] == "Apache-2.0" + + def test_dependency_groups_with_include(self): + dev_group = DependencyGroup( + name="dev", + include_groups=["test"], + dependencies=[DependencySpec(name="ruff", version_specs=[">=0.14.0"])], + ) + config = ProjectConfig( + name="test", + dependency_groups={"dev": dev_group}, + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + dev_items = content["dependency-groups"]["dev"] + assert {"include-group": "test"} in dev_items + assert "ruff>=0.14.0" in dev_items + # include-group should come before deps + assert dev_items[0] == {"include-group": "test"} + + def test_dependencies_in_output(self): + config = ProjectConfig( + name="test", + dependencies=[ + DependencySpec(name="click", version_specs=[">=8.0"]), + DependencySpec(name="rich", version_specs=[">=13.0"]), + ], + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + deps = content["project"]["dependencies"] + assert "click>=8.0" in deps + assert "rich>=13.0" in deps + + def test_optional_dependencies(self): + test_group = DependencyGroup( + name="test", + dependencies=[DependencySpec(name="pytest", version_specs=[">=8.0"])], + ) + config = ProjectConfig( + name="test", + optional_dependencies={"test": test_group}, + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + assert "pytest>=8.0" in content["project"]["optional-dependencies"]["test"] + + def test_removes_deprecated_uv_dev_dependencies(self): + config = ProjectConfig( + name="test", + tool_configs={"uv": {"dev-dependencies": ["pytest"], "other": "value"}}, + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + assert "dev-dependencies" not in content["tool"]["uv"] + assert content["tool"]["uv"]["other"] == "value" + + def test_removes_empty_uv_config(self): + config = ProjectConfig( + name="test", + tool_configs={"uv": {"dev-dependencies": ["pytest"]}}, + ) + generator = PyProjectGenerator(config) + content = generator.generate_toml_content() + + assert "uv" not in content.get("tool", {}) + + def test_write_to_file(self, tmp_path): + config = ProjectConfig( + name="test-project", + version="1.0.0", + description="Test", + license="MIT", + ) + generator = PyProjectGenerator(config) + output = tmp_path / "pyproject.toml" + generator.write_to_file(output, backup=False) + + with open(output, "rb") as f: + data = tomllib.load(f) + + assert data["project"]["name"] == "test-project" + assert data["project"]["license"] == "MIT" + + def test_write_creates_backup(self, tmp_path): + output = tmp_path / "pyproject.toml" + output.write_text("[project]\nname = 'old'\n") + + config = ProjectConfig(name="new") + generator = PyProjectGenerator(config) + generator.write_to_file(output, backup=True) + + backup = tmp_path / "pyproject.toml.backup" + assert backup.exists() + + +class TestPyProjectUpdater: + """Tests for PyProjectUpdater.""" + + def test_load_config_nonexistent_file(self, tmp_path): + updater = PyProjectUpdater(tmp_path / "nonexistent.toml", ConversionOptions()) + config = updater.load_config() + + assert config.name == "project-name" + + def test_load_config_basic(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[project]\nname = "myproject"\nversion = "2.0.0"\n' + 'requires-python = ">=3.12"\nlicense = "MIT"\n' + ) + + updater = PyProjectUpdater(pyproject, ConversionOptions()) + config = updater.load_config() + + assert config.name == "myproject" + assert config.version == "2.0.0" + assert config.requires_python == ">=3.12" + assert config.license == "MIT" + + def test_load_config_pep639_license_normalization(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[project]\nname = "test"\n\n[project.license]\ntext = "BSD-3-Clause"\n' + ) + + updater = PyProjectUpdater(pyproject, ConversionOptions()) + config = updater.load_config() + + assert config.license == "BSD-3-Clause" + + def test_load_config_dependency_groups_with_include(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[project]\nname = "test"\n\n' + "[dependency-groups]\n" + 'dev = [{include-group = "test"}, "ruff>=0.14.0"]\n' + 'test = ["pytest>=8.0"]\n' + ) + + updater = PyProjectUpdater(pyproject, ConversionOptions()) + config = updater.load_config() + + assert "dev" in config.dependency_groups + assert "test" in config.dependency_groups + dev = config.dependency_groups["dev"] + assert "test" in dev.include_groups + assert any(d.name == "ruff" for d in dev.dependencies) + + def test_load_config_with_dependencies(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[project]\nname = "test"\ndependencies = ["click>=8.0", "rich>=13.0"]\n' + ) + + updater = PyProjectUpdater(pyproject, ConversionOptions()) + config = updater.load_config() + + assert len(config.dependencies) == 2 + names = [d.name for d in config.dependencies] + assert "click" in names + assert "rich" in names + + def test_load_config_with_tool_configs(self, tmp_path): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[project]\nname = "test"\n\n[tool.ruff]\nline-length = 88\n' + ) + + updater = PyProjectUpdater(pyproject, ConversionOptions()) + config = updater.load_config() + + assert "ruff" in config.tool_configs + assert config.tool_configs["ruff"]["line-length"] == 88 + + def test_round_trip(self, tmp_path): + """Load a pyproject.toml, generate content, write it, and load again.""" + original = tmp_path / "pyproject.toml" + original.write_text( + '[build-system]\nrequires = ["hatchling"]\n' + 'build-backend = "hatchling.build"\n\n' + '[project]\nname = "roundtrip"\nversion = "1.0.0"\n' + 'description = "Test round trip"\n' + 'requires-python = ">=3.12"\n' + 'license = "MIT"\n' + 'dependencies = ["click>=8.0", "rich>=13.0"]\n\n' + "[dependency-groups]\n" + 'dev = [{include-group = "test"}, "ruff>=0.14.0"]\n' + 'test = ["pytest>=8.0"]\n' + ) + + updater = PyProjectUpdater(original, ConversionOptions()) + config = updater.load_config() + + generator = PyProjectGenerator(config) + output = tmp_path / "output.toml" + generator.write_to_file(output, backup=False) + + updater2 = PyProjectUpdater(output, ConversionOptions()) + config2 = updater2.load_config() + + assert config2.name == "roundtrip" + assert config2.version == "1.0.0" + assert config2.license == "MIT" + assert len(config2.dependencies) == 2 + assert "test" in config2.dependency_groups["dev"].include_groups diff --git a/tests/test_models.py b/tests/test_models.py index f3805ee..ac45d71 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -184,7 +184,7 @@ def test_create_basic_config(self): assert config.name == "test-project" assert config.version == "0.1.0" assert config.description == "" - assert config.requires_python == ">=3.8" + assert config.requires_python == ">=3.12" assert config.dependencies == [] assert config.optional_dependencies == {} assert config.dependency_groups == {} diff --git a/uv.lock b/uv.lock index d74de53..2d5866a 100644 --- a/uv.lock +++ b/uv.lock @@ -197,14 +197,13 @@ wheels = [ [[package]] name = "depcon" -version = "0.5.0" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "click" }, { name = "packaging" }, { name = "pydantic" }, { name = "rich" }, - { name = "tomli" }, { name = "tomli-w" }, ] @@ -235,7 +234,6 @@ requires-dist = [ { name = "packaging", specifier = ">=26.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "rich", specifier = ">=14.3.1" }, - { name = "tomli", specifier = ">=2.4.0" }, { name = "tomli-w", specifier = ">=1.2.0" }, ] @@ -878,51 +876,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - [[package]] name = "tomli-w" version = "1.2.0"