github.com/jfrog/build-info-go@v1.9.26/utils/pythonutils/poetryutils.go (about)

     1  package pythonutils
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/BurntSushi/toml"
     9  	"golang.org/x/exp/maps"
    10  )
    11  
    12  type PyprojectToml struct {
    13  	Tool map[string]PoetryPackage
    14  }
    15  type PoetryPackage struct {
    16  	Name            string
    17  	Version         string
    18  	Dependencies    map[string]interface{}
    19  	DevDependencies map[string]interface{} `toml:"dev-dependencies"`
    20  }
    21  
    22  type PoetryLock struct {
    23  	Package []*PoetryPackage
    24  }
    25  
    26  // Extract all poetry dependencies from the pyproject.toml and poetry.lock files.
    27  // Returns a dependency map of all the installed poetry packages in the current environment and another list of the top level dependencies.
    28  func getPoetryDependencies(srcPath string) (graph map[string][]string, directDependencies []string, err error) {
    29  	filePath, err := getPoetryLockFilePath(srcPath)
    30  	if err != nil || filePath == "" {
    31  		// Error was returned or poetry.lock does not exist in directory.
    32  		return map[string][]string{}, []string{}, err
    33  	}
    34  	projectName, directDependencies, err := getPackageNameFromPyproject(srcPath)
    35  	if err != nil {
    36  		return map[string][]string{}, []string{}, err
    37  	}
    38  	// Extract packages names from poetry.lock
    39  	dependencies, dependenciesVersions, err := extractPackagesFromPoetryLock(filePath)
    40  	if err != nil {
    41  		return map[string][]string{}, []string{}, err
    42  	}
    43  	graph = make(map[string][]string)
    44  	// Add the root node - the project itself.
    45  	for _, directDependency := range directDependencies {
    46  		directDependencyName := directDependency + ":" + dependenciesVersions[strings.ToLower(directDependency)]
    47  		graph[projectName] = append(graph[projectName], directDependencyName)
    48  	}
    49  	// Add versions to all dependencies
    50  	for dependency, transitiveDependencies := range dependencies {
    51  		for _, transitiveDependency := range transitiveDependencies {
    52  			transitiveDependencyName := transitiveDependency + ":" + dependenciesVersions[strings.ToLower(transitiveDependency)]
    53  			graph[dependency] = append(graph[dependency], transitiveDependencyName)
    54  		}
    55  	}
    56  	return graph, graph[projectName], nil
    57  }
    58  
    59  func getPackageNameFromPyproject(srcPath string) (string, []string, error) {
    60  	filePath, err := getPyprojectFilePath(srcPath)
    61  	if err != nil || filePath == "" {
    62  		// Error was returned or pyproject.toml does not exist in directory.
    63  		return "", []string{}, err
    64  	}
    65  	// Extract package name from pyproject.toml.
    66  	project, err := extractProjectFromPyproject(filePath)
    67  	if err != nil {
    68  		return "", []string{}, err
    69  	}
    70  	return project.Name, append(maps.Keys(project.Dependencies), maps.Keys(project.DevDependencies)...), nil
    71  }
    72  
    73  // Look for 'pyproject.toml' file in current work dir.
    74  // If found, return its absolute path.
    75  func getPyprojectFilePath(srcPath string) (string, error) {
    76  	return getFilePath(srcPath, "pyproject.toml")
    77  }
    78  
    79  // Look for 'poetry.lock' file in current work dir.
    80  // If found, return its absolute path.
    81  func getPoetryLockFilePath(srcPath string) (string, error) {
    82  	return getFilePath(srcPath, "poetry.lock")
    83  }
    84  
    85  // Get the project-name by parsing the pyproject.toml file.
    86  func extractProjectFromPyproject(pyprojectFilePath string) (project PoetryPackage, err error) {
    87  	content, err := os.ReadFile(pyprojectFilePath)
    88  	if err != nil {
    89  		return
    90  	}
    91  	var pyprojectFile PyprojectToml
    92  	_, err = toml.Decode(string(content), &pyprojectFile)
    93  	if err != nil {
    94  		return
    95  	}
    96  	if poetryProject, ok := pyprojectFile.Tool["poetry"]; ok {
    97  		// Extract project name from file content.
    98  		poetryProject.Name = poetryProject.Name + ":" + poetryProject.Version
    99  		return poetryProject, nil
   100  	}
   101  	return PoetryPackage{}, errors.New("Couldn't find project name and version in " + pyprojectFilePath)
   102  }
   103  
   104  // Get the project-name by parsing the poetry.lock file
   105  func extractPackagesFromPoetryLock(lockFilePath string) (dependencies map[string][]string, dependenciesVersions map[string]string, err error) {
   106  	content, err := os.ReadFile(lockFilePath)
   107  	if err != nil {
   108  		return
   109  	}
   110  	var poetryLockFile PoetryLock
   111  
   112  	_, err = toml.Decode(string(content), &poetryLockFile)
   113  	if err != nil {
   114  		return
   115  	}
   116  	dependenciesVersions = make(map[string]string)
   117  	dependencies = make(map[string][]string)
   118  	for _, dependency := range poetryLockFile.Package {
   119  		dependenciesVersions[strings.ToLower(dependency.Name)] = dependency.Version
   120  		dependencyName := dependency.Name + ":" + dependency.Version
   121  		dependencies[dependencyName] = maps.Keys(dependency.Dependencies)
   122  	}
   123  	return
   124  }