Skip to content

Commit d420e28

Browse files
ruromeroclaude
andcommitted
fix(pypi): address Qodo review findings for pyproject.toml support
- Fix PEP 621 annotation targeting: use individual TomlLiteral elements instead of parent TomlKeyValue so each dependency gets its own highlight - Fix quick-fix replacement: preserve package name in PEP 508 strings (e.g. "anyio==3.6.2" → "anyio==4.0.0" instead of just "==4.0.0") - Add [project.optional-dependencies] parsing for PEP 621 optional groups - Update README to reflect optional-dependencies are now analyzed Implements TC-3853 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Assisted-by: Claude Code
1 parent 64f3921 commit d420e28

3 files changed

Lines changed: 57 additions & 20 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,9 @@ When modifying the grammar or lexer files, you need to regenerate the parser cla
380380
You can create an alternative file to `requirements.txt`, for example, a `requirements-dev.txt` or
381381
a `requirements-test.txt` file where you can add the development or test dependencies there.
382382

383-
For `pyproject.toml`, only production dependencies from `[project.dependencies]` (PEP 621) and
384-
`[tool.poetry.dependencies]` are analyzed. Optional dependencies (`[project.optional-dependencies]`)
385-
and Poetry group dependencies (`[tool.poetry.group.*.dependencies]`) are excluded.
383+
For `pyproject.toml`, dependencies from `[project.dependencies]` (PEP 621),
384+
`[project.optional-dependencies]`, and `[tool.poetry.dependencies]` are analyzed.
385+
Poetry group dependencies (`[tool.poetry.group.*.dependencies]`) are excluded.
386386

387387

388388
- **Excluding manifest files with patterns**

src/main/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAAnnotator.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ protected Map<Dependency, List<PsiElement>> getDependencies(PsiFile file) {
8080
String tablePath = getTablePath(header);
8181
if ("project".equals(tablePath)) {
8282
parsePep621Dependencies(table, ignoredDeps, resultMap);
83+
} else if ("project.optional-dependencies".equals(tablePath)) {
84+
parseOptionalDependencies(table, ignoredDeps, resultMap);
8385
} else if ("tool.poetry.dependencies".equals(tablePath)) {
8486
parsePoetryDependencies(table, ignoredDeps, resultMap);
8587
}
@@ -100,15 +102,17 @@ protected CAUpdateManifestIntentionAction patchManifest(PsiElement element, Depe
100102

101103
@Override
102104
protected boolean isQuickFixApplicable(PsiElement element) {
103-
return element instanceof TomlKeyValue &&
104-
element.getContainingFile() != null &&
105-
PYPROJECT_TOML.equals(element.getContainingFile().getName());
105+
if (element.getContainingFile() == null || !PYPROJECT_TOML.equals(element.getContainingFile().getName())) {
106+
return false;
107+
}
108+
// Poetry: key-value pair like anyio = "^3.6.2"
109+
if (element instanceof TomlKeyValue) {
110+
return true;
111+
}
112+
// PEP 621: string literal in dependencies array like "anyio==3.6.2"
113+
return element instanceof TomlLiteral;
106114
}
107115

108-
/**
109-
* Parses PEP 621 [project] dependencies array.
110-
* Format: dependencies = ["anyio==3.6.2", "flask>=2.0.3"]
111-
*/
112116
private void parsePep621Dependencies(TomlTable projectTable, Set<String> ignoredDeps,
113117
Map<Dependency, List<PsiElement>> resultMap) {
114118
List<TomlKeyValue> keyValues = PsiTreeUtil.getChildrenOfTypeAsList(projectTable, TomlKeyValue.class);
@@ -131,10 +135,35 @@ private void parsePep621Dependencies(TomlTable projectTable, Set<String> ignored
131135
}
132136
String version = extractPep508Version(depString);
133137
Dependency dp = new Dependency(PYPI, null, name.toLowerCase(), version);
134-
// Use the parent TomlKeyValue of the array as element for annotation,
135-
// but we need to find or create a suitable element. Use the literal itself
136-
// wrapped through the array's parent key-value.
137-
resultMap.computeIfAbsent(dp, k -> new LinkedList<>()).add(kv);
138+
resultMap.computeIfAbsent(dp, k -> new LinkedList<>()).add(literal);
139+
}
140+
}
141+
}
142+
143+
/**
144+
* Parses PEP 621 [project.optional-dependencies] table.
145+
* Format: dev = ["pytest>=7.0", "black"], security = ["certifi>=2023.7"]
146+
*/
147+
private void parseOptionalDependencies(TomlTable optDepsTable, Set<String> ignoredDeps,
148+
Map<Dependency, List<PsiElement>> resultMap) {
149+
List<TomlKeyValue> groups = PsiTreeUtil.getChildrenOfTypeAsList(optDepsTable, TomlKeyValue.class);
150+
for (TomlKeyValue group : groups) {
151+
TomlValue value = group.getValue();
152+
if (!(value instanceof TomlArray array)) {
153+
continue;
154+
}
155+
for (TomlValue element : array.getElements()) {
156+
if (!(element instanceof TomlLiteral literal)) {
157+
continue;
158+
}
159+
String depString = unquote(literal.getText());
160+
String name = extractPep508Name(depString);
161+
if (name == null || ignoredDeps.contains(name.toLowerCase())) {
162+
continue;
163+
}
164+
String version = extractPep508Version(depString);
165+
Dependency dp = new Dependency(PYPI, null, name.toLowerCase(), version);
166+
resultMap.computeIfAbsent(dp, k -> new LinkedList<>()).add(literal);
138167
}
139168
}
140169
}
@@ -280,7 +309,7 @@ private String normalizeKeyName(String keyText) {
280309
return keyText;
281310
}
282311

283-
private static String unquote(String text) {
312+
static String unquote(String text) {
284313
if (text != null && text.length() >= 2) {
285314
if ((text.startsWith("\"") && text.endsWith("\"")) ||
286315
(text.startsWith("'") && text.endsWith("'"))) {

src/main/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAIntentionAction.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ protected void updateVersion(@NotNull Project project, Editor editor, PsiFile fi
4040
return;
4141
}
4242

43-
if (element instanceof TomlKeyValue keyValue) {
43+
if (element instanceof TomlLiteral literal) {
44+
// PEP 621: string literal in dependencies array like "anyio==3.6.2"
45+
String depString = PyprojectCAAnnotator.unquote(literal.getText());
46+
String name = PyprojectCAAnnotator.extractPep508Name(depString);
47+
if (name != null) {
48+
replaceVersionLiteral(project, file, literal, name + "==" + version);
49+
}
50+
} else if (element instanceof TomlKeyValue keyValue) {
51+
// Poetry: key-value pair like anyio = "^3.6.2"
4452
TomlLiteral valueLiteral = findVersionLiteral(keyValue);
4553
if (valueLiteral != null) {
4654
replaceVersionLiteral(project, file, valueLiteral, "==" + version);
@@ -50,12 +58,11 @@ protected void updateVersion(@NotNull Project project, Editor editor, PsiFile fi
5058

5159
private TomlLiteral findVersionLiteral(TomlKeyValue keyValue) {
5260
if (keyValue.getValue() instanceof TomlLiteral literal) {
53-
// Simple string: "anyio==3.6.2" — this is a PEP 508 array entry, version is embedded in the string
54-
// For array entries, the whole literal is the dependency string
61+
// Poetry simple string: anyio = "^3.6.2"
5562
return literal;
5663
}
5764
if (keyValue.getValue() instanceof TomlInlineTable inlineTable) {
58-
// Poetry format: anyio = {version = "^3.6.2"}
65+
// Poetry inline table: anyio = {version = "^3.6.2"}
5966
for (TomlKeyValue entry : PsiTreeUtil.getChildrenOfTypeAsList(inlineTable, TomlKeyValue.class)) {
6067
if ("version".equals(entry.getKey().getText()) && entry.getValue() instanceof TomlLiteral) {
6168
return (TomlLiteral) entry.getValue();
@@ -94,6 +101,7 @@ private void replaceVersionLiteral(@NotNull Project project, PsiFile file, TomlL
94101

95102
@Override
96103
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
97-
return file != null && "pyproject.toml".equals(file.getName()) && element instanceof TomlKeyValue;
104+
return file != null && "pyproject.toml".equals(file.getName())
105+
&& (element instanceof TomlKeyValue || element instanceof TomlLiteral);
98106
}
99107
}

0 commit comments

Comments
 (0)