Skip to content
Closed
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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Have you actually looked at the code/run a scan on the test file to see this is not already detected somewhat?
See https://github.com/aboutcode-org/scancode-toolkit/blob/develop/src/packagedcode/conda.py#L357 where we already support this (but this needs enhancement of course)

Yes we should have updated/closed #3694 when this was added, but you should always do basic checks and read the code before writing code blindly.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You need to use real examples seen in the wild, not made up examples.
See examples already in https://github.com/aboutcode-org/scancode-toolkit/tree/develop/tests/packagedcode/data/conda/conda-yaml

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not how we run tests, see https://github.com/aboutcode-org/scancode-toolkit/blob/develop/tests/packagedcode/test_conda.py, where we have sample tests, and that is where the new tests will live.

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