Skip to content

Commit

Permalink
#161 ✨ migration dev-dependencies to group.dev.dependencies automatic…
Browse files Browse the repository at this point in the history
…ally
  • Loading branch information
d-ryan-ashcraft committed Jun 25, 2024
1 parent ba03791 commit 829ff26
Show file tree
Hide file tree
Showing 24 changed files with 412 additions and 199 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ Optional set of dependencies to manage across modules extending a parent pom. Th
specific version, which is often useful to ensure that information assurance patches, common versions, etc. are enforced
across a series of modules. Can be used with the next several variables to control automatic update, logging, or failing
the build when mismatches are found between the managed dependency operator/version and what is currently specified.
Looks at dependencies in `[tool.poetry.dependencies]`, `[tool.poetry.dev-dependencies]`, and any
Looks at dependencies in `[tool.poetry.dependencies]`, `[tool.poetry.group.dev.dependencies]`, and any
`[tool.poetry.group.<subgroup>]` of your `pyproject.toml`.
```xml
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ protected void processManagedDependencyMismatches() {
pyProjectConfig.load();

// Look for the standard Poetry dependency groups:
executeDetailedManagedDependencyMismatchActions(replacements, pyProjectConfig, "tool.poetry.dependencies");
executeDetailedManagedDependencyMismatchActions(replacements, pyProjectConfig, "tool.poetry.dev-dependencies");
executeDetailedManagedDependencyMismatchActions(replacements, pyProjectConfig, TomlUtils.TOOL_POETRY_DEPENDENCIES);
executeDetailedManagedDependencyMismatchActions(replacements, pyProjectConfig, TomlUtils.TOOL_POETRY_GROUP_MONOREPO_DEPENDENCIES);

// Search for custom Poetry dependency groups:
List<String> toolPoetryGroupSections = findCustomToolPoetryGroups();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package org.technologybrewery.habushu.migration;

import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.file.FileConfig;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.technologybrewery.baton.AbstractMigration;
import org.technologybrewery.baton.BatonException;
import org.technologybrewery.habushu.HabushuException;
import org.technologybrewery.habushu.util.TomlReplacementTuple;
import org.technologybrewery.habushu.util.TomlUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* Common logic to migrate TOML file entries from one group to another.
*/
public abstract class AbstractTomlGroupMigration extends AbstractMigration {

public static final Logger logger = LoggerFactory.getLogger(AbstractTomlGroupMigration.class);

protected Map<String, TomlReplacementTuple> replacements = new HashMap<>();

private boolean hasExistingNewGroup = false;

protected abstract String getLegacyGroupName();

protected abstract String getNewGroupName();

@Override
protected boolean shouldExecuteOnFile(File file) {
replacements.clear();
boolean shouldExecute = false;
try (FileConfig tomlFileConfig = FileConfig.of(file)) {
tomlFileConfig.load();

String legacyGroup = getLegacyGroupName();
Optional<Config> legacyGroupEntries = tomlFileConfig.getOptional(legacyGroup);
if (legacyGroupEntries.isPresent()) {
Config foundGroupEntries = legacyGroupEntries.get();
Map<String, Object> groupEntryMap = foundGroupEntries.valueMap();

for (Map.Entry<String, Object> groupEntry : groupEntryMap.entrySet()) {
String groupEntryName = groupEntry.getKey();
Object groupEntryRhs = groupEntry.getValue();
String groupEntryRshAsString = null;
if (groupEntryRhs instanceof CommentedConfig) {
groupEntryRshAsString = TomlUtils.convertCommentedConfigToToml((CommentedConfig) groupEntryRhs);
} else {
groupEntryRshAsString = (String) groupEntryRhs;
}
logger.info("Found [{}] group entry to migrate! ({} = {})", legacyGroup, groupEntryName, groupEntryRshAsString);
TomlReplacementTuple replacementTuple = new TomlReplacementTuple(groupEntryName, groupEntryRshAsString, "");
replacements.put(groupEntryName, replacementTuple);
}

String newGroupName = getNewGroupName();
Optional<Config> newGroupEntries = tomlFileConfig.getOptional(newGroupName);
hasExistingNewGroup = newGroupEntries.isPresent();
shouldExecute = true;
}
}

return shouldExecute;
}

@Override
protected boolean performMigration(File pyProjectTomlFile) {
String fileContent = StringUtils.EMPTY;
String newGroupName = getNewGroupName();

try (BufferedReader reader = new BufferedReader(new FileReader(pyProjectTomlFile))) {
String line = reader.readLine();
boolean injectAfterNextEmptyLine = false;

while (line != null) {
boolean addLine = true;
boolean isEmptyLine = line.isBlank();

if (line.contains(StringUtils.SPACE) && line.contains(TomlUtils.EQUALS)) {
String key = line.substring(0, line.indexOf(StringUtils.SPACE));

if (key == null) {
key = line.substring(0, line.indexOf(TomlUtils.EQUALS));
}

if (key != null) {
key = key.strip();

TomlReplacementTuple matchedTuple = replacements.get(key);
if (matchedTuple != null) {
// skip this line, we will add it back to [tool.poetry.dependencies] later
addLine = false;
}
}

} else if (line.contains("[") && line.contains("]")) {
String key = line.strip();

if (hasExistingNewGroup && (key.equals("[" + newGroupName + "]"))) {
// skip this line as we are overriding with the line plus dependencies here:
addLine = false;
fileContent += line + "\n";
fileContent = injectDependencies(fileContent);
} else if (!hasExistingNewGroup) {
injectAfterNextEmptyLine = true;
}

String legacyGroupName = getLegacyGroupName();
if ((key.equals("[" + legacyGroupName + "]"))){
addLine = false;
}
}

if (isEmptyLine && injectAfterNextEmptyLine) {
fileContent += "\n[" + newGroupName + "]" + "\n";
fileContent = injectDependencies(fileContent);
injectAfterNextEmptyLine = false;
}

if (addLine) {
fileContent += line + "\n";
}

line = reader.readLine();
}

} catch (IOException e) {
throw new HabushuException("Problem reading pyproject.toml to migrate groups!", e);
}

try {
TomlUtils.writeTomlFile(pyProjectTomlFile, fileContent);
} catch (IOException e) {
throw new BatonException("Problem moving monorepo dependencies to [" + newGroupName + "]!", e);
}

return true;

}

private String injectDependencies(String fileContent) {
for (Map.Entry<String, TomlReplacementTuple> entry : replacements.entrySet()) {
fileContent += entry.getKey() + " = " + TomlUtils.escapeTomlRightHandSide(entry.getValue().getOriginalOperatorAndVersion()) + "\n";
}
return fileContent;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.technologybrewery.habushu.migration;

import org.technologybrewery.habushu.util.TomlUtils;

/**
* Migrates the [tool.poetry.dev-dependencies] group to [tool.poetry.group.dev.dependencies] per preferred Poetry 1.2.0+
* approach as noted on https://python-poetry.org/docs/managing-dependencies/.
*/
public class DevDependenciesGroupMigration extends AbstractTomlGroupMigration {

@Override
protected String getLegacyGroupName() {
return "tool.poetry.dev-dependencies";
}

@Override
protected String getNewGroupName() {
return TomlUtils.TOOL_POETRY_DEV_DEPENDENCIES;
}
}
Original file line number Diff line number Diff line change
@@ -1,150 +1,21 @@
package org.technologybrewery.habushu.migration;

import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.ConfigFormat;
import com.electronwill.nightconfig.core.file.FileConfig;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.technologybrewery.baton.AbstractMigration;
import org.technologybrewery.baton.BatonException;
import org.technologybrewery.habushu.HabushuException;
import org.technologybrewery.habushu.util.TomlReplacementTuple;
import org.technologybrewery.habushu.util.TomlUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* Automatically migrates any monorepo dependencies (e.g., foo = {path = "../foo", develop = true}) in the main
* [tool.poetry.dependencies] group into the [tool.poetry.group.monorepo.dependencies] group instead. As noted in the
* project's README.md, this prevents these dependencies from causing issues when Poetry projects are exported in
* development releases.
* Automatically migrates any monorepo dependencies (e.g., foo = {path = "../foo", develop = true}) in the
* [tool.poetry.group.monorepo.dependencies] group into the [tool.poetry.dependencies] group instead.
*/
public class RemoveMonorepoGroupMigration extends AbstractMigration {

public static final Logger logger = LoggerFactory.getLogger(RemoveMonorepoGroupMigration.class);

protected Map<String, TomlReplacementTuple> replacements = new HashMap<>();

private boolean hasExistingDependenciesGroup = false;
public class RemoveMonorepoGroupMigration extends AbstractTomlGroupMigration {

@Override
protected boolean shouldExecuteOnFile(File file) {
replacements.clear();
boolean shouldExecute = false;
try (FileConfig tomlFileConfig = FileConfig.of(file)) {
tomlFileConfig.load();

Optional<Config> toolPoetryMonorepoDependencies = tomlFileConfig.getOptional(TomlUtils.TOOL_POETRY_GROUP_MONOREPO_DEPENDENCIES);
if (toolPoetryMonorepoDependencies.isPresent()) {
Config foundDependencies = toolPoetryMonorepoDependencies.get();
Map<String, Object> dependencyMap = foundDependencies.valueMap();

for (Map.Entry<String, Object> dependency : dependencyMap.entrySet()) {
String packageName = dependency.getKey();
Object packageRhs = dependency.getValue();
String packageRshAsString = null;
if (packageRhs instanceof CommentedConfig) {
packageRshAsString = TomlUtils.convertCommentedConfigToToml((CommentedConfig) packageRhs);
} else {
packageRshAsString = (String) packageRhs;
}
logger.info("Found local dependency within monorepo group! ({} = {})", packageName, packageRshAsString);
TomlReplacementTuple replacementTuple = new TomlReplacementTuple(packageName, packageRshAsString, "");
replacements.put(packageName, replacementTuple);
}
Optional<Config> toolPoetryDependecies = tomlFileConfig.getOptional(TomlUtils.TOOL_POETRY_DEPENDENCIES);
hasExistingDependenciesGroup = toolPoetryDependecies.isPresent();
shouldExecute = true;
}
}

return shouldExecute;
protected String getLegacyGroupName() {
return TomlUtils.TOOL_POETRY_GROUP_MONOREPO_DEPENDENCIES;
}

@Override
protected boolean performMigration(File pyProjectTomlFile) {
String fileContent = StringUtils.EMPTY;
try (BufferedReader reader = new BufferedReader(new FileReader(pyProjectTomlFile))) {
String line = reader.readLine();
boolean injectAfterNextEmptyLine = false;

while (line != null) {
boolean addLine = true;
boolean isEmptyLine = line.isBlank();

if (line.contains(StringUtils.SPACE) && line.contains(TomlUtils.EQUALS)) {
String key = line.substring(0, line.indexOf(StringUtils.SPACE));

if (key == null) {
key = line.substring(0, line.indexOf(TomlUtils.EQUALS));
}

if (key != null) {
key = key.strip();

TomlReplacementTuple matchedTuple = replacements.get(key);
if (matchedTuple != null) {
// skip this line, we will add it back to [tool.poetry.dependencies] later
addLine = false;
}
}

} else if (line.contains("[") && line.contains("]")) {
String key = line.strip();

if (hasExistingDependenciesGroup && (key.equals("[" + TomlUtils.TOOL_POETRY_DEPENDENCIES + "]"))) {
// skip this line as we are overriding with the line plus dependencies here:
addLine = false;
fileContent += line + "\n";
fileContent = injectDependencies(fileContent);
} else if (!hasExistingDependenciesGroup) {
injectAfterNextEmptyLine = true;
}

if ((key.equals("[" + TomlUtils.TOOL_POETRY_GROUP_MONOREPO_DEPENDENCIES + "]"))){
addLine = false;
}
}

if (isEmptyLine && injectAfterNextEmptyLine) {
fileContent += "\n[" + TomlUtils.TOOL_POETRY_DEPENDENCIES + "]" + "\n";
fileContent = injectDependencies(fileContent);
injectAfterNextEmptyLine = false;
}

if (addLine) {
fileContent += line + "\n";
}

line = reader.readLine();
}

} catch (IOException e) {
throw new HabushuException("Problem reading pyproject.toml to update with managed dependencies!", e);
}

try {
TomlUtils.writeTomlFile(pyProjectTomlFile, fileContent);
} catch (IOException e) {
throw new BatonException("Problem moving monorepo dependencies to [tool.poetry.dependencies]!", e);
}

return true;

protected String getNewGroupName() {
return TomlUtils.TOOL_POETRY_DEPENDENCIES;
}

private String injectDependencies(String fileContent) {
for (Map.Entry<String, TomlReplacementTuple> entry : replacements.entrySet()) {
fileContent += entry.getKey() + " = " + TomlUtils.escapeTomlRightHandSide(entry.getValue().getOriginalOperatorAndVersion()) + "\n";
}
return fileContent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class TomlUtils {
public static final String EQUALS = "=";
public static final String DOUBLE_QUOTE = "\"";
public static final String TOOL_POETRY_DEPENDENCIES = "tool.poetry.dependencies";
public static final String TOOL_POETRY_DEV_DEPENDENCIES = "tool.poetry.group.dev.dependencies";
public static final String TOOL_POETRY_GROUP_MONOREPO_DEPENDENCIES = "tool.poetry.group.monorepo.dependencies";
public static final String VERSION = "version";
public static final String PATH = "path";
Expand Down
Loading

0 comments on commit 829ff26

Please sign in to comment.