Skip to content

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

Open
Strum355 wants to merge 1 commit intoTC-4264from
TC-4266
Open

feat(workspace): add uv workspace discovery (TC-4266)#446
Strum355 wants to merge 1 commit intoTC-4264from
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.

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.

TC-4266

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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 on lines +1080 to +1081
if (PyprojectTomlUtils.getProjectName(toml) != null) {
manifests.addFirst(rootPyproject);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +1085 to +1086
} catch (Exception e) {
LOG.warning("Failed to discover uv workspace members: " + e.getMessage());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

      return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifests, ignorePatterns);
    } catch (Exception e) {
      LOG.warning("Failed to discover uv workspace members: " + e.getMessage());
      return Collections.emptyList();
    }
      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).

Comment on lines +33 to +42
@Test
void discoverWorkspaceManifests_uvRootPackageWorkspace() throws IOException {
Path workspaceDir = UV_FIXTURES.resolve("uv_workspace").toAbsolutePath().normalize();

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

assertThat(manifests).hasSize(3);
assertThat(manifests).allMatch(p -> p.toString().endsWith("pyproject.toml"));
assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pyproject.toml"));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

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

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