Skip to content

feat(workspace): add uv workspace discovery (TC-4266)#446

Open
Strum355 wants to merge 1 commit intomainfrom
TC-4266
Open

feat(workspace): add uv workspace discovery (TC-4266)#446
Strum355 wants to merge 1 commit intomainfrom
TC-4266

Conversation

@Strum355
Copy link
Copy Markdown
Member

@Strum355 Strum355 commented Apr 28, 2026

Summary

  • Parse [tool.uv.workspace] member and exclude globs from pyproject.toml to discover workspace member manifests
  • Virtual workspaces (no [project] section) correctly exclude root pyproject.toml from results
  • Add __pycache__ and .venv to default ignore patterns
  • Reuse existing PyprojectTomlUtils and WorkspaceUtils for TOML parsing and ignore filtering

Test plan

  • 7 unit tests covering root-package workspace, virtual workspace, exclude patterns, nested multi-glob patterns, no-lock, no-config, and ignore pattern filtering
  • All existing workspace tests continue to pass

TC-4266

🤖 Generated with Claude Code

Summary by Sourcery

Add support for discovering Python uv workspaces during workspace manifest resolution and extend default ignore patterns.

New Features:

  • Discover uv workspace member pyproject.toml files based on [tool.uv.workspace] configuration and uv.lock presence.
  • Support both root-package and virtual uv workspaces when resolving workspace manifests.

Enhancements:

  • Extend default workspace discovery ignore patterns to skip pycache and .venv directories.

Tests:

  • Add unit test coverage for uv workspace discovery including root, virtual, excluded, nested, missing-lock, missing-config, and ignore-pattern scenarios.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 28, 2026

🧙 Sourcery has finished reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In discoverUvWorkspaceMembers, manifests is an ArrayList but later addFirst(rootPyproject) is called, which will not compile; consider using manifests.add(0, rootPyproject) or switching manifests to a LinkedList.
  • The Files.walk(workspaceDir) in discoverUvWorkspaceMembers does not use the ignore patterns to prune traversal (they are only applied after discovery via WorkspaceUtils.filterByIgnorePatterns), which could lead to unnecessary scanning of large directories like node_modules or .git; consider incorporating ignore patterns into the walk itself.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `discoverUvWorkspaceMembers`, `manifests` is an `ArrayList` but later `addFirst(rootPyproject)` is called, which will not compile; consider using `manifests.add(0, rootPyproject)` or switching `manifests` to a `LinkedList`.
- The `Files.walk(workspaceDir)` in `discoverUvWorkspaceMembers` does not use the ignore patterns to prune traversal (they are only applied after discovery via `WorkspaceUtils.filterByIgnorePatterns`), which could lead to unnecessary scanning of large directories like `node_modules` or `.git`; consider incorporating ignore patterns into the walk itself.

## Individual Comments

### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java" line_range="1080-1081" />
<code_context>
+                });
+      }
+
+      if (PyprojectTomlUtils.getProjectName(toml) != null) {
+        manifests.addFirst(rootPyproject);
+      }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** ArrayList does not support addFirst; this line will not compile or will behave incorrectly if the type changes later.

To fix this, either change `manifests` to a `LinkedList`/`Deque` implementation, or keep it as an `ArrayList` and use `manifests.add(0, rootPyproject);` instead. If maintaining discovery order is important, consider documenting that intent in the surrounding code or comments.
</issue_to_address>

### Comment 2
<location path="src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java" line_range="1085-1086" />
<code_context>
+      }
+
+      return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifests, ignorePatterns);
+    } catch (Exception e) {
+      LOG.warning("Failed to discover uv workspace members: " + e.getMessage());
+      return Collections.emptyList();
+    }
</code_context>
<issue_to_address>
**suggestion:** Logging only the exception message may make diagnosing discovery failures difficult.

Using only `e.getMessage()` drops the stack trace and root cause, which makes diagnosing workspace discovery failures much harder. Prefer logging the full exception (e.g., `LOG.log(Level.WARNING, "Failed to discover uv workspace members", e)`) so production issues remain debuggable while still returning an empty list.

Suggested implementation:

```java
      return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifests, ignorePatterns);
    } catch (Exception e) {
      LOG.warning("Failed to discover uv workspace members: " + e.getMessage());
      return Collections.emptyList();
    }

```

```java
      return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifests, ignorePatterns);
    } catch (Exception e) {
      LOG.log(Level.WARNING, "Failed to discover uv workspace members", e);
      return Collections.emptyList();
    }

  private static final Set<String> DEFAULT_WORKSPACE_DISCOVERY_IGNORE =

```

1. Ensure that `java.util.logging.Level` is imported at the top of `ExhortApi.java`, e.g.:
   `import java.util.logging.Level;`
2. If the logger is not a `java.util.logging.Logger` but another logging framework (e.g., SLF4J), adjust the logging call accordingly (for SLF4J it would be `LOG.warn("Failed to discover uv workspace members", e);` instead of using `Level`).
</issue_to_address>

### Comment 3
<location path="src/test/java/io/github/guacsec/trustifyda/impl/UvWorkspaceDiscoveryTest.java" line_range="33-42" />
<code_context>
+                        "libs" + File.separator + "core" + File.separator + "pyproject.toml"));
+  }
+
+  @Test
+  void discoverWorkspaceManifests_uvNoLockFile() throws IOException {
+    Path workspaceDir = UV_FIXTURES.resolve("uv_workspace_no_lock").toAbsolutePath().normalize();
+
+    ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class));
+    List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());
+
+    assertThat(manifests).isEmpty();
+  }
+
+  @Test
+  void discoverWorkspaceManifests_uvNoWorkspaceConfig() throws IOException {
+    Path workspaceDir = UV_FIXTURES.resolve("uv_workspace_no_config").toAbsolutePath().normalize();
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for uv workspace configs with empty or whitespace-only `members`/`exclude` arrays

`discoverUvWorkspaceMembers` explicitly ignores null/blank entries in `members` and `exclude`, and returns an empty result when `memberPatterns` is empty. Existing tests cover no workspace config and normal members, but not the case where `[tool.uv.workspace]` exists and these arrays contain only empty or whitespace-only strings. Please add a fixture and test for that scenario to verify the filtering logic and avoid treating such configs as valid members in future.

Suggested implementation:

```java
  @Test
  void discoverWorkspaceManifests_uvWhitespaceOnlyMembersAndExclude() throws IOException {
    Path workspaceDir =
        UV_FIXTURES.resolve("uv_workspace_whitespace_only").toAbsolutePath().normalize();

    ExhortApi api = new ExhortApi(Mockito.mock(java.net.http.HttpClient.class));
    List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());

    assertThat(manifests).isEmpty();
  }
}

```

1. Add a new test fixture directory at `src/test/resources/tst_manifests/workspace/uv/uv_workspace_whitespace_only`.
2. Inside that directory, create a `pyproject.toml` (or equivalent workspace config used by `discoverUvWorkspaceMembers`) containing a `[tool.uv.workspace]` section where both `members` and `exclude` are arrays with only empty and/or whitespace-only strings, for example:
   - `members = ["", " ", "   "]`
   - `exclude = ["", "  "]`
3. Ensure that this fixture does not contain any other valid member patterns that would cause `discoverUvWorkspaceMembers` to return non-empty `memberPatterns`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java
Comment thread src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java Outdated
@Strum355
Copy link
Copy Markdown
Member Author

Strum355 commented May 5, 2026

Verification Report — TC-4266

PR: #446 | Branch: TC-4266main | Verdict: PASS

Review Feedback

# Thread Classification Action
1 sourcery-ai: ArrayList.addFirst() bug risk False positive (Java 21+ API) No action
2 sourcery-ai: logging suggestion Resolved (commit b3a48df) No action
3 sourcery-ai: empty members test suggestion Nice-to-have No action

All 3 threads classified in prior run; no new threads.

CI Status

46/46 checks PASS

Scope Containment

Single clean commit 8602496 — uv workspace discovery only.
Files modified: ExhortApi.java (uv detection + discoverUvWorkspaceMembers() method), WorkspaceUtils.java (glob fix for root-level paths), UvWorkspaceDiscoveryTest.java, plus 15 test fixture files.

Previously: PR had wrong base branch (TC-4264) and included Go workspace commits. Fixed by rebasing onto main and squashing to a single uv-only commit. Base branch updated to main.

Sensitive Patterns

No secrets, credentials, or API keys detected.

Acceptance Criteria

# Criterion Status
1 stackAnalysisBatch() discovers all member pyproject.toml files PASS
2 TOML parsing extracts [tool.uv.workspace].members and .exclude PASS
3 Glob expansion of member patterns works with Java PathMatcher PASS
4 Virtual workspaces exclude root from manifests PASS
5 Root-package workspaces include root in manifests PASS
6 **/__pycache__/** and **/.venv/** added to default ignore patterns PASS
7 Missing uv.lock → not detected as workspace PASS

Test Quality

7 tests in UvWorkspaceDiscoveryTest.java — all pass. Covers: simple workspace, virtual workspace (root excluded), root-package workspace (root included), exclude patterns, nested member discovery, no workspace config, and missing uv.lock.

Verification Commands

Command Result
mvn test -Dtest=UvWorkspaceDiscoveryTest PASS (7 tests, 0 failures)

This comment was AI-generated by sdlc-workflow/verify-pr v0.5.11. Updated after branch cleanup (rebase onto main, single clean commit).

@Strum355 Strum355 changed the base branch from TC-4264 to main May 5, 2026 16:50
Parse [tool.uv.workspace] member and exclude globs from pyproject.toml
to discover all workspace member manifests. Virtual workspaces (no
[project] section) correctly exclude the root pyproject.toml. Adds
__pycache__ and .venv to default ignore patterns.

Also fix Java PathMatcher quirk where **/X/** doesn't match root-level
paths in WorkspaceUtils.filterByIgnorePatterns.

TC-4266

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Test Results

460 tests   460 ✅  1m 41s ⏱️
 31 suites    0 💤
 31 files      0 ❌

Results for commit 8602496.

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.

1 participant