Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/packagedcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
conda.CondaMetaJsonHandler,
conda.CondaMetaYamlHandler,
conda.CondaYamlHandler,
conda.CondaEnvironmentYmlHandler,

conan.ConanFileHandler,
conan.ConanDataHandler,
Expand Down
76 changes: 76 additions & 0 deletions src/packagedcode/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,79 @@ def get_variables(location):
parts = line.split('=')
result[parts[0].strip()] = parts[-1].strip().strip('"')
return result


class CondaEnvironmentYmlHandler(BaseDependencyFileHandler):
datasource_id = 'conda_environment_yml'
path_patterns = ('*environment.yml', '*environment.yaml')
default_package_type = 'conda'
description = 'Conda environment.yml'

@classmethod
def parse(cls, location, package_only=False):
import re
with open(location) as fi:
conda_data = saneyaml.load(fi.read())

if not conda_data or not isinstance(conda_data, dict):
conda_data = {}

name = conda_data.get('name')
extra_data = {}
channels = conda_data.get('channels')
if channels:
extra_data['channels'] = channels

dependencies = []
deps_data = conda_data.get('dependencies') or []
for dep in deps_data:
if isinstance(dep, str):
match = re.split(r'(>=|<=|==|=|>|<)', dep, maxsplit=1)
dep_name = match[0].strip()
version = None

if len(match) > 1:
version = match[2].strip()

purl = PackageURL(type='conda', name=dep_name)

dependencies.append(
models.DependentPackage(
purl=purl.to_string(),
extracted_requirement=version,
scope='runtime',
is_runtime=True,
is_optional=False,
)
)
elif isinstance(dep, dict) and 'pip' in dep:
pip_deps = dep.get('pip') or []
for pip_dep in pip_deps:
match = re.split(r'(>=|<=|==|=|>|<|~=)', pip_dep, maxsplit=1)
pip_name = match[0].strip()
version = None
if len(match) > 1:
version = match[2].strip()

purl = PackageURL(type='pypi', name=pip_name)

dependencies.append(
models.DependentPackage(
purl=purl.to_string(),
extracted_requirement=version,
scope='runtime',
is_runtime=True,
is_optional=False,
)
)

package_data = dict(
datasource_id=cls.datasource_id,
type=cls.default_package_type,
name=name,
dependencies=dependencies,
)
if extra_data:
package_data['extra_data'] = extra_data

yield models.PackageData.from_data(package_data, package_only)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: myenv
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- numpy=1.24.0
- pandas
- pip:
- requests==2.28.0
- flask>=2.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: testenv
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- numpy=1.24.0
- pip:
- requests==2.28.0
63 changes: 63 additions & 0 deletions tests/packagedcode/test_conda_environment_yml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os
from packages_test_utils import PackageTester
from packagedcode import conda
from scancode_config import REGEN_TEST_FIXTURES

class TestCondaEnvironmentYml(PackageTester):
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')

def test_conda_environment_yml_is_datafile(self):
test_file = self.get_test_loc('conda/environment_yml/simple-environment.yml')
assert conda.CondaEnvironmentYmlHandler.is_datafile(test_file)

def test_parse_simple_environment_yml(self):
test_file = self.get_test_loc('conda/environment_yml/simple-environment.yml')
packages = list(conda.CondaEnvironmentYmlHandler.parse(test_file))
assert len(packages) == 1
package = packages[0]

assert package.name == 'testenv'
assert package.extra_data == {'channels': ['conda-forge', 'defaults']}

deps = package.dependencies

numpy_dep = next((d for d in deps if d.purl == 'pkg:conda/numpy'), None)
assert numpy_dep is not None
assert numpy_dep.extracted_requirement == '1.24.0'

requests_dep = next((d for d in deps if d.purl == 'pkg:pypi/requests'), None)
assert requests_dep is not None
assert requests_dep.extracted_requirement == '2.28.0'

def test_parse_real_environment_yml(self):
test_file = self.get_test_loc('conda/environment_yml/multiregex-environment.yml')
packages = list(conda.CondaEnvironmentYmlHandler.parse(test_file))
assert len(packages) == 1
package = packages[0]

assert package.name == 'myenv'
assert len(package.dependencies) > 0
deps = [d.purl for d in package.dependencies]
assert 'pkg:conda/pandas' in deps

def test_parse_empty_dependencies(self):
test_file = self.get_temp_file('empty-deps.yml')
with open(test_file, 'w') as f:
f.write('name: nodeps\nchannels:\n - defaults\ndependencies:\n')

packages = list(conda.CondaEnvironmentYmlHandler.parse(test_file))
assert len(packages) == 1
package = packages[0]
assert package.name == 'nodeps'
assert package.dependencies == []

def test_parse_missing_name(self):
test_file = self.get_temp_file('noname.yml')
with open(test_file, 'w') as f:
f.write('channels:\n - defaults\ndependencies:\n - python=3.10\n')

packages = list(conda.CondaEnvironmentYmlHandler.parse(test_file))
assert len(packages) == 1
package = packages[0]
assert package.name is None
assert len(package.dependencies) == 1
Loading