diff --git a/fawltydeps/extract_deps/environment_yml_parser.py b/fawltydeps/extract_deps/environment_yml_parser.py index 5c14f992..59833695 100644 --- a/fawltydeps/extract_deps/environment_yml_parser.py +++ b/fawltydeps/extract_deps/environment_yml_parser.py @@ -1,6 +1,7 @@ """Code for parsing dependencies from environment.yml files.""" import logging +import re from pathlib import Path from typing import Any, Callable, Dict, Iterator, List, Union @@ -13,6 +14,13 @@ YamlDependencyData = Union[List[str], Dict[str, "YamlDependencyData"], Any, None] # type: ignore[misc] +class InvalidCondaRequirement(ValueError): # noqa: N818 + """Invalid Conda dependency specifier. + + Modeled on packaging.requirements.InvalidRequirement. + """ + + def parse_one_conda_dep(req_str: str, source: Location) -> DeclaredDependency: """Parse a single Conda dependency string. @@ -24,9 +32,13 @@ def parse_one_conda_dep(req_str: str, source: Location) -> DeclaredDependency: _, req_str = req_str.split("::", 1) if "[" in req_str: # remove bracket stuff from back req_str, _ = req_str.split("[]", 1) - req_str, *_ = req_str.split() # remove anything after whitespace - name, *_ = req_str.split("=") # remove version/build info - return DeclaredDependency(name, source) + # package name comes before version/build info, which starts with one of ">!~]+", req_str) + if not name_match: + raise InvalidCondaRequirement( + f"Expected package name at the start of dependency specifier: {req_str!r}" + ) + return DeclaredDependency(name_match.group(0), source) def parse_environment_yml_deps( @@ -47,7 +59,11 @@ def parse_environment_yml_deps( return for dep_item in parsed_deps: if isinstance(dep_item, str): - yield parse_item(dep_item, source) + try: + yield parse_item(dep_item, source) + except ValueError as e: + # InvalidCondaRequirement or packaging.requirements.InvalidRequirement + logger.error(f"{error_msg} {e}") elif isinstance(dep_item, dict) and len(dep_item) == 1 and "pip" in dep_item: pip_deps = dep_item.get("pip") yield from parse_environment_yml_deps( diff --git a/tests/test_extract_deps_environment_yml.py b/tests/test_extract_deps_environment_yml.py index 1458bfee..6ffeed37 100644 --- a/tests/test_extract_deps_environment_yml.py +++ b/tests/test_extract_deps_environment_yml.py @@ -155,6 +155,70 @@ ["scikit-learn", "pip"], id="mixed_conda_and_zero_pip_deps", ), + pytest.param( + """\ + # To set up a development environment using conda, run: + # + # conda env create -f environment.yml + # conda activate cartopy-dev + # pip install -e . + # + name: cartopy-dev + channels: + - conda-forge + dependencies: + - cython>=0.29.28 + - numpy>=1.23 + - shapely>=2.0 + - pyshp>=2.3 + - pyproj>=3.3.1 + - packaging>=21 + # The testing label has the proper version of freetype included + - conda-forge/label/testing::matplotlib-base>=3.6 + + # OWS + - owslib>=0.27 + - pillow>=9.1 + # Plotting + - scipy>=1.9 + # Testing + - pytest + - pytest-mpl + - pytest-xdist + # Documentation + - pydata-sphinx-theme + - sphinx + - sphinx-gallery + # Extras + - pre-commit + - pykdtree + - ruff + - setuptools_scm + """, + [ + "cython", + "numpy", + "shapely", + "pyshp", + "pyproj", + "packaging", + "matplotlib-base", + "owslib", + "pillow", + "scipy", + "pytest", + "pytest-mpl", + "pytest-xdist", + "pydata-sphinx-theme", + "sphinx", + "sphinx-gallery", + "pre-commit", + "pykdtree", + "ruff", + "setuptools_scm", + ], + id="cartopy_example", + ), ], ) def test_parse_environment_yml__wellformed_dependencies__yields_dependencies( @@ -239,6 +303,27 @@ class CondaTestVector: error_msg_fragment="Failed to parse Pip dependencies in {path}: Not a string: ", expect=["foo", "bar"], ), + CondaTestVector( + id="invalid_dependencies_malformed_names", + data="""\ + dependencies: + - ">foo<" + - "bar" + """, + error_msg_fragment="Failed to parse Conda dependencies in {path}: Expected package name at the start of dependency specifier", + expect=["bar"], + ), + CondaTestVector( + id="invalid_dependencies_malformed_names", + data="""\ + dependencies: + - "pip": + - "~foo" + - "bar" + """, + error_msg_fragment="Failed to parse Pip dependencies in {path}: Expected package name at the start of dependency specifier", + expect=["bar"], + ), ]