Skip to content

Commit 7f0213c

Browse files
authored
Merge pull request #3584 from mashehu/broaden-channel-linting
2 parents 03e7dec + d7c9b30 commit 7f0213c

File tree

4 files changed

+96
-163
lines changed

4 files changed

+96
-163
lines changed

nf_core/modules/lint/environment_yml.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,28 @@ def environment_yml(module_lint_object: ComponentLint, module: NFCoreComponent,
8383

8484
if valid_env_yml:
8585
# Check that the dependencies section is sorted alphabetically
86-
if sorted(env_yml["dependencies"]) == env_yml["dependencies"]:
86+
def sort_recursively(obj):
87+
"""Simple recursive sort for nested structures."""
88+
if isinstance(obj, list):
89+
90+
def get_key(x):
91+
if isinstance(x, dict):
92+
# For dicts like {"pip": [...]}, use the key "pip"
93+
return (list(x.keys())[0], 1)
94+
else:
95+
# For strings like "pip=23.3.1", use "pip" and for bioconda::samtools=1.15.1, use "bioconda::samtools"
96+
return (str(x).split("=")[0], 0)
97+
98+
return sorted([sort_recursively(item) for item in obj], key=get_key)
99+
elif isinstance(obj, dict):
100+
return {k: sort_recursively(v) for k, v in obj.items()}
101+
else:
102+
return obj
103+
104+
sorted_dependencies = sort_recursively(env_yml["dependencies"])
105+
106+
# Direct comparison of sorted vs original dependencies
107+
if sorted_dependencies == env_yml["dependencies"]:
87108
module.passed.append(
88109
(
89110
"environment_yml_sorted",
@@ -96,6 +117,6 @@ def environment_yml(module_lint_object: ComponentLint, module: NFCoreComponent,
96117
log.info(
97118
f"Dependencies in {module.component_name}'s environment.yml were not sorted alphabetically. Sorting them now."
98119
)
99-
env_yml["dependencies"].sort()
120+
env_yml["dependencies"] = sorted_dependencies
100121
with open(Path(module.component_dir, "environment.yml"), "w") as fh:
101122
yaml.dump(env_yml, fh, Dumper=custom_yaml_dumper())

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ jsonschema>=4.0
77
markdown>=3.3
88
packaging
99
pillow
10-
pdiff
1110
pre-commit
1211
prompt_toolkit<=3.0.51
1312
pydantic>=2.2.1

tests/modules/test_lint.py

Lines changed: 70 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -394,35 +394,10 @@ def test_modules_lint_snapshot_file_not_needed(self):
394394

395395
def test_modules_environment_yml_file_doesnt_exists(self):
396396
"""Test linting a module with an environment.yml file"""
397-
Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "environment.yml").rename(
398-
Path(
399-
self.nfcore_modules,
400-
"modules",
401-
"nf-core",
402-
"bpipe",
403-
"test",
404-
"environment.yml.bak",
405-
)
406-
)
397+
(self.bpipe_test_module_path / "environment.yml").rename(self.bpipe_test_module_path / "environment.yml.bak")
407398
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
408399
module_lint.lint(print_results=False, module="bpipe/test")
409-
Path(
410-
self.nfcore_modules,
411-
"modules",
412-
"nf-core",
413-
"bpipe",
414-
"test",
415-
"environment.yml.bak",
416-
).rename(
417-
Path(
418-
self.nfcore_modules,
419-
"modules",
420-
"nf-core",
421-
"bpipe",
422-
"test",
423-
"environment.yml",
424-
)
425-
)
400+
(self.bpipe_test_module_path / "environment.yml.bak").rename(self.bpipe_test_module_path / "environment.yml")
426401
assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
427402
assert len(module_lint.passed) > 0
428403
assert len(module_lint.warned) >= 0
@@ -438,32 +413,13 @@ def test_modules_environment_yml_file_sorted_correctly(self):
438413

439414
def test_modules_environment_yml_file_sorted_incorrectly(self):
440415
"""Test linting a module with an incorrectly sorted environment.yml file"""
441-
with open(
442-
Path(
443-
self.nfcore_modules,
444-
"modules",
445-
"nf-core",
446-
"bpipe",
447-
"test",
448-
"environment.yml",
449-
)
450-
) as fh:
416+
with open(self.bpipe_test_module_path / "environment.yml") as fh:
451417
yaml_content = yaml.safe_load(fh)
452418
# Add a new dependency to the environment.yml file and reverse the order
453419
yaml_content["dependencies"].append("z=0.0.0")
454420
yaml_content["dependencies"].reverse()
455421
yaml_content = yaml.dump(yaml_content)
456-
with open(
457-
Path(
458-
self.nfcore_modules,
459-
"modules",
460-
"nf-core",
461-
"bpipe",
462-
"test",
463-
"environment.yml",
464-
),
465-
"w",
466-
) as fh:
422+
with open(self.bpipe_test_module_path / "environment.yml", "w") as fh:
467423
fh.write(yaml_content)
468424
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
469425
module_lint.lint(print_results=False, module="bpipe/test")
@@ -474,44 +430,77 @@ def test_modules_environment_yml_file_sorted_incorrectly(self):
474430

475431
def test_modules_environment_yml_file_not_array(self):
476432
"""Test linting a module with an incorrectly formatted environment.yml file"""
477-
with open(
478-
Path(
479-
self.nfcore_modules,
480-
"modules",
481-
"nf-core",
482-
"bpipe",
483-
"test",
484-
"environment.yml",
485-
)
486-
) as fh:
433+
with open(self.bpipe_test_module_path / "environment.yml") as fh:
487434
yaml_content = yaml.safe_load(fh)
488435
yaml_content["dependencies"] = "z"
489-
with open(
490-
Path(
491-
self.nfcore_modules,
492-
"modules",
493-
"nf-core",
494-
"bpipe",
495-
"test",
496-
"environment.yml",
497-
),
498-
"w",
499-
) as fh:
436+
with open(self.bpipe_test_module_path / "environment.yml", "w") as fh:
437+
fh.write(yaml.dump(yaml_content))
438+
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
439+
module_lint.lint(print_results=False, module="bpipe/test")
440+
assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
441+
assert len(module_lint.passed) > 0
442+
assert len(module_lint.warned) >= 0
443+
assert module_lint.failed[0].lint_test == "environment_yml_valid"
444+
445+
def test_modules_environment_yml_file_mixed_dependencies(self):
446+
"""Test linting a module with mixed-type dependencies (strings and pip dict)"""
447+
with open(self.bpipe_test_module_path / "environment.yml") as fh:
448+
yaml_content = yaml.safe_load(fh)
449+
450+
# Create mixed dependencies with strings and pip dict in wrong order
451+
yaml_content["dependencies"] = [
452+
"python=3.8",
453+
{"pip": ["zzz-package==1.0.0", "aaa-package==2.0.0"]},
454+
"bioconda::samtools=1.15.1",
455+
"bioconda::fastqc=0.12.1",
456+
"pip=23.3.1",
457+
]
458+
459+
with open(self.bpipe_test_module_path / "environment.yml", "w") as fh:
500460
fh.write(yaml.dump(yaml_content))
461+
501462
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
502463
module_lint.lint(print_results=False, module="bpipe/test")
464+
465+
# Check that the dependencies were sorted correctly
466+
with open(self.bpipe_test_module_path / "environment.yml") as fh:
467+
sorted_yaml = yaml.safe_load(fh)
468+
469+
expected_deps = [
470+
"bioconda::fastqc=0.12.1",
471+
"bioconda::samtools=1.15.1",
472+
"pip=23.3.1",
473+
{"pip": ["aaa-package==2.0.0", "zzz-package==1.0.0"]},
474+
"python=3.8",
475+
]
476+
477+
assert sorted_yaml["dependencies"] == expected_deps
478+
assert len(module_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
479+
assert len(module_lint.passed) > 0
480+
assert len(module_lint.warned) >= 0
481+
482+
def test_modules_environment_yml_file_default_channel_fails(self):
483+
"""Test linting a module with a default channel set in the environment.yml file, which should fail"""
484+
with open(self.bpipe_test_module_path / "environment.yml") as fh:
485+
yaml_content = yaml.safe_load(fh)
486+
yaml_content["channels"] = ["bioconda", "default"]
487+
with open(self.bpipe_test_module_path / "environment.yml", "w") as fh:
488+
fh.write(yaml.dump(yaml_content))
489+
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
490+
module_lint.lint(print_results=False, module="bpipe/test")
491+
503492
assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
504493
assert len(module_lint.passed) > 0
505494
assert len(module_lint.warned) >= 0
506495
assert module_lint.failed[0].lint_test == "environment_yml_valid"
507496

508497
def test_modules_meta_yml_incorrect_licence_field(self):
509498
"""Test linting a module with an incorrect Licence field in meta.yml"""
510-
with open(Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml")) as fh:
499+
with open(self.bpipe_test_module_path / "meta.yml") as fh:
511500
meta_yml = yaml.safe_load(fh)
512501
meta_yml["tools"][0]["bpipe"]["licence"] = "[MIT]"
513502
with open(
514-
Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml"),
503+
self.bpipe_test_module_path / "meta.yml",
515504
"w",
516505
) as fh:
517506
fh.write(yaml.dump(meta_yml))
@@ -589,45 +578,13 @@ def test_modules_missing_test_dir(self):
589578

590579
def test_modules_missing_test_main_nf(self):
591580
"""Test linting a module with a missing test/main.nf file"""
592-
Path(
593-
self.nfcore_modules,
594-
"modules",
595-
"nf-core",
596-
"bpipe",
597-
"test",
598-
"tests",
599-
"main.nf.test",
600-
).rename(
601-
Path(
602-
self.nfcore_modules,
603-
"modules",
604-
"nf-core",
605-
"bpipe",
606-
"test",
607-
"tests",
608-
"main.nf.test.bak",
609-
)
581+
(self.bpipe_test_module_path / "tests" / "main.nf.test").rename(
582+
self.bpipe_test_module_path / "tests" / "main.nf.test.bak"
610583
)
611584
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
612585
module_lint.lint(print_results=False, module="bpipe/test")
613-
Path(
614-
self.nfcore_modules,
615-
"modules",
616-
"nf-core",
617-
"bpipe",
618-
"test",
619-
"tests",
620-
"main.nf.test.bak",
621-
).rename(
622-
Path(
623-
self.nfcore_modules,
624-
"modules",
625-
"nf-core",
626-
"bpipe",
627-
"test",
628-
"tests",
629-
"main.nf.test",
630-
)
586+
(self.bpipe_test_module_path / "tests" / "main.nf.test.bak").rename(
587+
self.bpipe_test_module_path / "tests" / "main.nf.test"
631588
)
632589
assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
633590
assert len(module_lint.passed) >= 0
@@ -666,46 +623,15 @@ def test_nftest_failing_linting(self):
666623

667624
def test_modules_absent_version(self):
668625
"""Test linting a nf-test module if the versions is absent in the snapshot file `"""
669-
with open(
670-
Path(
671-
self.nfcore_modules,
672-
"modules",
673-
"nf-core",
674-
"bpipe",
675-
"test",
676-
"tests",
677-
"main.nf.test.snap",
678-
)
679-
) as fh:
626+
snap_file = self.bpipe_test_module_path / "tests" / "main.nf.test.snap"
627+
with open(snap_file) as fh:
680628
content = fh.read()
681629
new_content = content.replace("versions", "foo")
682-
with open(
683-
Path(
684-
self.nfcore_modules,
685-
"modules",
686-
"nf-core",
687-
"bpipe",
688-
"test",
689-
"tests",
690-
"main.nf.test.snap",
691-
),
692-
"w",
693-
) as fh:
630+
with open(snap_file, "w") as fh:
694631
fh.write(new_content)
695632
module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules)
696633
module_lint.lint(print_results=False, module="bpipe/test")
697-
with open(
698-
Path(
699-
self.nfcore_modules,
700-
"modules",
701-
"nf-core",
702-
"bpipe",
703-
"test",
704-
"tests",
705-
"main.nf.test.snap",
706-
),
707-
"w",
708-
) as fh:
634+
with open(snap_file, "w") as fh:
709635
fh.write(content)
710636
assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}"
711637
assert len(module_lint.passed) >= 0
@@ -714,15 +640,7 @@ def test_modules_absent_version(self):
714640

715641
def test_modules_empty_file_in_snapshot(self):
716642
"""Test linting a nf-test module with an empty file sha sum in the test snapshot, which should make it fail (if it is not a stub)"""
717-
snap_file = Path(
718-
self.nfcore_modules,
719-
"modules",
720-
"nf-core",
721-
"bpipe",
722-
"test",
723-
"tests",
724-
"main.nf.test.snap",
725-
)
643+
snap_file = self.bpipe_test_module_path / "tests" / "main.nf.test.snap"
726644
snap = json.load(snap_file.open())
727645
content = snap_file.read_text()
728646
snap["my test"]["content"][0]["0"] = "test:md5,d41d8cd98f00b204e9800998ecf8427e"
@@ -743,15 +661,7 @@ def test_modules_empty_file_in_snapshot(self):
743661

744662
def test_modules_empty_file_in_stub_snapshot(self):
745663
"""Test linting a nf-test module with an empty file sha sum in the stub test snapshot, which should make it not fail"""
746-
snap_file = Path(
747-
self.nfcore_modules,
748-
"modules",
749-
"nf-core",
750-
"bpipe",
751-
"test",
752-
"tests",
753-
"main.nf.test.snap",
754-
)
664+
snap_file = self.bpipe_test_module_path / "tests" / "main.nf.test.snap"
755665
snap = json.load(snap_file.open())
756666
content = snap_file.read_text()
757667
snap["my_test_stub"] = {"content": [{"0": "test:md5,d41d8cd98f00b204e9800998ecf8427e", "versions": {}}]}

tests/test_modules.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ def setUp(self):
160160
# Set up the nf-core/modules repo dummy
161161
self.nfcore_modules = create_modules_repo_dummy(self.tmp_dir)
162162

163+
# Common path to the bpipe/test module used in tests
164+
self.bpipe_test_module_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test")
165+
163166
def test_modulesrepo_class(self):
164167
"""Initialise a modules repo object"""
165168
modrepo = nf_core.modules.modules_repo.ModulesRepo()

0 commit comments

Comments
 (0)