Skip to content

feat: add support for pyproject.toml via uv, poetry & pdm#432

Open
Strum355 wants to merge 1 commit intoguacsec:mainfrom
Strum355:nsc/pyproject
Open

feat: add support for pyproject.toml via uv, poetry & pdm#432
Strum355 wants to merge 1 commit intoguacsec:mainfrom
Strum355:nsc/pyproject

Conversation

@Strum355
Copy link
Copy Markdown
Member

@Strum355 Strum355 commented Mar 25, 2026

Description

Adds support for pyproject.toml files, using lockfiles from uv, poetry or pdm.

Implements: TC-3850

Checklist

  • I have followed this repository's contributing guidelines.
  • I will adhere to the project's code of conduct.

@Strum355 Strum355 requested a review from ruromero March 25, 2026 12:19
@qodo-code-review
Copy link
Copy Markdown

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Add pyproject.toml support with uv, poetry, and pdm lock file providers

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add new Python provider supporting pyproject.toml with uv, poetry, and pdm lock files
• Parse direct dependencies from PEP 621 project.dependencies or tool.poetry.dependencies
• Support dependency tree building with transitive dependency resolution
• Implement PEP 503 package name canonicalization for matching manifest and lock file names
• Support exhortignore and trustify-da-ignore markers for excluding dependencies
• Handle PEP 508 features including extras and environment markers
• Add comprehensive test coverage with multiple lock file scenarios
• Minor code formatting fixes in python_pip.js provider
Diagram
flowchart LR
  A["pyproject.toml"] --> B["Lock File Detection"]
  B --> C["poetry.lock"]
  B --> D["pdm.lock"]
  B --> E["uv.lock"]
  C --> F["Parse Dependencies"]
  D --> F
  E --> F
  F --> G["Build Dependency Tree"]
  G --> H["Apply Ignore Markers"]
  H --> I["Generate SBOM"]
Loading

Grey Divider

File Changes

1. src/provider.js ✨ Enhancement +2/-0

Register new Python pyproject provider

src/provider.js


2. src/providers/python_pyproject.js ✨ Enhancement +351/-0

New provider for pyproject.toml support

src/providers/python_pyproject.js


3. src/providers/python_pip.js Formatting +4/-4

Code formatting and spacing fixes

src/providers/python_pip.js


View more (17)
4. test/provider.test.js 🧪 Tests +7/-0

Add pyproject.toml provider matching tests

test/provider.test.js


5. test/providers/python_pyproject.test.js 🧪 Tests +170/-0

Comprehensive test suite for pyproject provider

test/providers/python_pyproject.test.js


6. test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml 🧪 Tests +11/-0

Test fixture for poetry lock scenario

test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml


7. test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json 🧪 Tests +48/-0

Expected SBOM for poetry component analysis

test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json


8. test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json 🧪 Tests +147/-0

Expected SBOM for poetry stack analysis

test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json


9. test/providers/tst_manifests/pyproject/pdm_lock/pyproject.toml 🧪 Tests +9/-0

Test fixture for pdm lock scenario

test/providers/tst_manifests/pyproject/pdm_lock/pyproject.toml


10. test/providers/tst_manifests/pyproject/pdm_lock/expected_component_sbom.json 🧪 Tests +48/-0

Expected SBOM for pdm component analysis

test/providers/tst_manifests/pyproject/pdm_lock/expected_component_sbom.json


11. test/providers/tst_manifests/pyproject/pdm_lock/expected_stack_sbom.json 🧪 Tests +147/-0

Expected SBOM for pdm stack analysis

test/providers/tst_manifests/pyproject/pdm_lock/expected_stack_sbom.json


12. test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml 🧪 Tests +7/-0

Test fixture for uv lock scenario

test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml


13. test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json 🧪 Tests +48/-0

Expected SBOM for uv component analysis

test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json


14. test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json 🧪 Tests +147/-0

Expected SBOM for uv stack analysis

test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json


15. test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml 🧪 Tests +10/-0

Test fixture for poetry-only dependencies

test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml


16. test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json 🧪 Tests +60/-0

Expected SBOM for poetry-only component analysis

test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json


17. test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json 🧪 Tests +160/-0

Expected SBOM for poetry-only stack analysis

test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json


18. test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml 🧪 Tests +11/-0

Test fixture for PEP 508 features and ignore markers

test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml


19. test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json 🧪 Tests +72/-0

Expected SBOM for PEP 621 component analysis

test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json


20. test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json 🧪 Tests +158/-0

Expected SBOM for PEP 621 stack analysis

test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 25, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Remediation recommended

1. Lockfile gating inconsistent 🐞 Bug ⛯ Reliability
Description
python_pyproject.validateLockFile() always returns true, so match() will select the provider
even when no lock file exists and the failure is deferred to SBOM generation with a different error
path/message than other providers.
Code

src/providers/python_pyproject.js[R27-29]

+function validateLockFile() { return true }
+
+/**
Evidence
match() relies on each provider’s validateLockFile(manifestDir, opts) as the gate for “manifest
supported + required lock file present”; when no provider passes, it throws a consistent lockfile
error. The pyproject provider returns true unconditionally and only checks for lock files later in
createSbom(), so match() cannot perform early lockfile validation and users see a different
error message and timing compared to other ecosystems (e.g., Cargo tests assert match() throws
when lockfile is missing).

src/provider.js[60-70]
src/providers/python_pyproject.js[27-29]
src/providers/python_pyproject.js[327-330]
test/providers/rust_cargo.test.js[105-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/provider.js::match()` uses `validateLockFile(manifestDir, opts)` to gate provider selection and to throw a consistent lockfile-required error early. The new `python_pyproject` provider returns `true` unconditionally and instead throws later from `createSbom()` when it can’t find a lock file, leading to inconsistent behavior and error messaging.

### Issue Context
- The pyproject provider already has `findLockFile(manifestDir, parsed)` which can be reused for validation.
- `match()` passes `opts` to `validateLockFile` to support workspace-root lockfile patterns used by other providers.

### Fix Focus Areas
- src/providers/python_pyproject.js[27-109]
- src/providers/python_pyproject.js[322-330]
- src/provider.js[60-70]

### Implementation sketch
- Change `validateLockFile(manifestDir, opts = {})` to:
 - Try to read/parse `pyproject.toml` in `manifestDir` (or accept manifestDir only and just check for any of `poetry.lock|pdm.lock|uv.lock`).
 - Return `true` only if a supported lockfile is present (and optionally matches declared tool).
 - Optionally honor `TRUSTIFY_DA_WORKSPACE_DIR` similarly to other providers (see base_javascript/rust_cargo patterns).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Workspace lockfiles unsupported 🐞 Bug ⚙ Maintainability
Description
The pyproject provider only searches for lock files in the manifest directory and does not consult
TRUSTIFY_DA_WORKSPACE_DIR, so workspace/monorepo layouts with a root lock file can fail to
match/validate consistently with other providers.
Code

src/providers/python_pyproject.js[R89-107]

+function findLockFile(manifestDir, parsed) {
+	let hasPoetry = !!(parsed.tool?.poetry)
+	let hasPdm = !!(parsed.tool?.pdm)
+
+	let poetryLock = path.join(manifestDir, 'poetry.lock')
+	let pdmLock = path.join(manifestDir, 'pdm.lock')
+	let uvLock = path.join(manifestDir, 'uv.lock')
+
+	// prefer lock file matching the declared tool
+	if (hasPoetry && fs.existsSync(poetryLock)) { return { type: 'poetry', path: poetryLock } }
+	if (hasPdm && fs.existsSync(pdmLock)) { return { type: 'pdm', path: pdmLock } }
+
+	// then check uv (no pyproject.toml marker needed)
+	if (fs.existsSync(uvLock)) { return { type: 'uv', path: uvLock } }
+
+	// fallback: any available lock file
+	if (fs.existsSync(poetryLock)) { return { type: 'poetry', path: poetryLock } }
+	if (fs.existsSync(pdmLock)) { return { type: 'pdm', path: pdmLock } }
+
Evidence
Other ecosystems implement workspace-root lockfile lookup via TRUSTIFY_DA_WORKSPACE_DIR in
validateLockFile() (and match() explicitly passes opts for that). The pyproject provider’s
lockfile discovery (findLockFile) is hard-coded to manifestDir, and validateLockFile ignores
parameters entirely, so it cannot support the same workspace override mechanism.

src/providers/python_pyproject.js[27-27]
src/providers/python_pyproject.js[89-107]
src/provider.js[60-70]
src/providers/base_javascript.js[114-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`python_pyproject` lockfile lookup is limited to the manifest directory. In workspace/monorepo setups, lock files often live at the workspace root, and the rest of the codebase supports this pattern via `TRUSTIFY_DA_WORKSPACE_DIR`.

### Issue Context
- `src/provider.js::match()` passes `opts` to `validateLockFile` for workspace overrides.
- `base_javascript` and `rust_cargo` demonstrate the expected pattern.

### Fix Focus Areas
- src/providers/python_pyproject.js[27-109]
- src/providers/python_pyproject.js[322-330]

### Implementation sketch
- Update `validateLockFile(manifestDir, opts = {})` to use `TRUSTIFY_DA_WORKSPACE_DIR` (if set) as the directory to check for `poetry.lock|pdm.lock|uv.lock`.
- Update `createSbom()` / `findLockFile()` to also consider the same directory when `TRUSTIFY_DA_WORKSPACE_DIR` is provided, so validation and execution use identical rules.
- Add/adjust a test scenario with `opts.TRUSTIFY_DA_WORKSPACE_DIR` to prevent regressions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo


const IGNORE_MARKERS = ['exhortignore', 'trustify-da-ignore']

const DEFAULT_ROOT_NAME = 'default-pip-root'
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesnt seem to ever be changed, its a relic copied from the pip provider. Pyproject.toml has project specific metadata that we could probably extract a more meaningful root name from.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, [project] metadata for name and version are required in the toml file so it should be used instead of this default.
this is something to implement also in the Java side

@ruromero
Copy link
Copy Markdown
Collaborator

ruromero commented Mar 25, 2026

Verification Report for TC-3850 (Revised)

Check Result Details
Scope Containment WARN README.md listed in task but missing from PR. Out-of-scope: src/providers/python_pip.js, test/provider.test.js, 20 test fixture files
Diff Size WARN 1980 additions, 4 deletions across 25 files — large relative to 4 expected files, though bulk is test fixtures
Commit Traceability FAIL Single commit "feat: add support for pyproject.toml via uv, poetry & pdm" does not reference TC-3850
Sensitive Patterns PASS No sensitive patterns detected
CI Status PASS All 4 checks pass (Lint/test Node 22, Lint/test Node 24, Validate PR title, Validate commit messages)
Acceptance Criteria FAIL 5 of 6 criteria met (see below)
Verification Commands N/A No verification commands specified

Acceptance Criteria Details

# Criterion Result
1 pyproject.toml recognized as supported manifest PASS — isSupported returns true, provider registered in availableProviders
2 Dependencies from [project.dependencies] (PEP 621) correctly parsed PASS — PEP 621 parsing at lines 68-70
3 Dependencies from [tool.poetry.dependencies] correctly parsed PASS — Poetry parsing at lines 75-77
4 Provider generates valid pypi PURLs PASS — new PackageURL('pypi', ...) at line 278
5 Stack and component analysis work end-to-end PASS — provideStack and provideComponent implemented with tests
6 README.md updated to document pyproject.toml support FAIL — README.md not modified in this PR

Architecture Comparison with Java Counterpart (PR #358)

This PR was cross-referenced against the Java client's pyproject.toml implementation (guacsec/trustify-da-java-client#358).

Resolution strategy — justified divergence from task spec:

The Jira task instructs to "reuse Python_controller class for dependency tree resolution." However, this PR takes a different — and arguably correct — approach:

Aspect requirements.txt (python_pip.js) pyproject.toml (this PR) Java pyproject (PR #358)
Lock file None — requirements.txt is a flat list Yes (poetry.lock / pdm.lock / uv.lock) None — delegates to pip
Resolution pip installpip freezepip show Reads lock file directly pip installpip freezepip show
Requires Python Yes No Yes
Requires binaries pip None pip

The requirements.txt provider uses Python_controller because requirements.txt has no lock file — pip must be invoked to resolve the dependency tree. pyproject.toml does have lock files (poetry.lock, pdm.lock, uv.lock) which already contain the fully resolved tree. Reading the lock file directly is the correct approach — it's faster, requires no Python installation, and mirrors how npm/Cargo providers work in this codebase.

The Java implementation takes a different path by converting pyproject.toml deps to a temp requirements.txt and piping through pip — this works but doesn't leverage the lock file.

Findings

1. Lock file validation gap (WARN)

  • validateLockFile() always returns true — no check for lock file existence or freshness
  • buildDependencyTree() silently skips direct deps not found in the lock file (if (!entry) { continue })
  • A stale or missing lock file will produce a silently incomplete SBOM
  • Recommendation: validateLockFile(manifestDir) should check that at least one lock file exists in the manifest directory, consistent with how base_javascript.js and rust_cargo.js validate their lock files

2. Code duplication (WARN)

  • toPurl() and addAllDependencies() are duplicated verbatim from python_pip.js
  • Default root component constants are duplicated with slightly different names (DEFAULT_ROOT_NAME vs DEFAULT_PIP_ROOT_COMPONENT_NAME)
  • Recommendation: Extract shared Python utilities into a common module

3. Silent dependency loss (WARN)

  • If a dep is in pyproject.toml but not in the lock file, it is silently dropped — no error, no warning
  • This differs from python_pip.js which throws: "Package ${name} is not installed in your python environment"
  • Recommendation: Log a warning or throw when a manifest dep is missing from the lock file

4. README.md not updated (FAIL)

  • The task requires updating Python support docs, exhortignore examples, Match Manifest Versions section, virtual environment section, and CLI examples

5. Missing Jira traceability (FAIL)

  • Commit message does not reference TC-3850 (required by §2.1 constraints)

Overall: FAIL

Core implementation is solid and the lock-file-based architecture is a sound design choice. Two blocking issues remain: missing README.md updates and missing Jira traceability. Three non-blocking recommendations for lock file validation, code deduplication, and silent dependency handling.


This comment was AI-generated by sdlc-workflow/verify-pr v0.5.0.

Copy link
Copy Markdown
Collaborator

@ruromero ruromero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested by the review tool.
The readme changes are missing and we have to align the Java and Javascript implementations.
I suggest that the best approach is to leverage the tooling to resolve the dependencies and make sure the lock file is aligned, as we do in the Java implementation.

The poetry, uv and pdm lockfiles were added but if we rely on the binary files to resolve the dependencies and ensure the lock file sync we will have to implement the support individually. Let's create separate tasks for uv and poetry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants