Skip to content

Commit 10a3374

Browse files
committed
Add environment.yml support to the rest of FawltyDeps
Now that we can parse dependency declarations from environment.yml files, we need to expose this functionality in our docs + CLI, as well as automatically find environment.yml files while traversing projects.
1 parent 0ca230f commit 10a3374

File tree

8 files changed

+43
-3
lines changed

8 files changed

+43
-3
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ dependencies. A number of file formats are supported:
130130
`extras_require` arguments)
131131
- `setup.cfg`
132132
- `pixi.toml`
133+
- `environment.yml`
133134

134135
The `--deps` option accepts a space-separated list of files or directories.
135136
Each file will be parsed for declared dependencies; each directory will
@@ -437,8 +438,8 @@ Here is a complete list of configuration directives we support:
437438
in the repository.
438439
- `deps_parser_choice`: Manually select which format to use for parsing
439440
declared dependencies. Must be one of `"requirements.txt"`, `"setup.py"`,
440-
`"setup.cfg"`, `"pyproject.toml"`, `"pixi.toml"`, or leave it unset
441-
(i.e. the default) for auto-detection (based on filename).
441+
`"setup.cfg"`, `"pyproject.toml"`, `"pixi.toml"`, `"environment.yml"`, or
442+
leave it unset (i.e. the default) for auto-detection (based on filename).
442443
- `install-deps`: Automatically install Python dependencies gathered with
443444
FawltyDeps into a temporary virtual environment. This will use `uv` or `pip`
444445
to download and install packages from PyPI by default.

fawltydeps/extract_deps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Callable, Iterable, Iterator, NamedTuple, Optional
77

8+
from fawltydeps.extract_deps_environment_yml import parse_environment_yml
89
from fawltydeps.extract_deps_pixi import parse_pixi_toml
910
from fawltydeps.extract_deps_pyproject import parse_pyproject_toml
1011
from fawltydeps.extract_deps_requirements import parse_requirements_txt
@@ -53,6 +54,9 @@ def first_applicable_parser(path: Path) -> Optional[ParserChoice]:
5354
ParserChoice.PIXI_TOML: ParsingStrategy(
5455
lambda path: path.name == "pixi.toml", parse_pixi_toml
5556
),
57+
ParserChoice.ENVIRONMENT_YML: ParsingStrategy(
58+
lambda path: path.name == "environment.yml", parse_environment_yml
59+
),
5660
}
5761

5862

fawltydeps/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class ParserChoice(Enum):
4545
SETUP_CFG = "setup.cfg"
4646
PYPROJECT_TOML = "pyproject.toml"
4747
PIXI_TOML = "pixi.toml"
48+
ENVIRONMENT_YML = "environment.yml"
4849

4950
def __str__(self) -> str:
5051
return self.value

tests/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def fake_project(write_tmp_files, fake_venv): # noqa: C901
164164
lists of strings (extras/optional deps).
165165
The dependencies will be written into associated files, formatted
166166
according to the filenames (must be one of requirements.txt, setup.py,
167-
setup.cfg, pyproject.toml, or pixi.toml).
167+
setup.cfg, pyproject.toml, pixi.toml, or environment.yml).
168168
- extra_file_contents: a dict with extra files and their associated contents
169169
to be forwarded directly to write_tmp_files().
170170
@@ -231,6 +231,16 @@ def format_pixi_toml(deps: Deps, extras: ExtraDeps) -> str:
231231
ret += "\n".join(f'{dep} = "*"' for dep in deps)
232232
return ret
233233

234+
def format_environment_yml(deps: Deps, no_extras: ExtraDeps) -> str:
235+
assert not no_extras # not supported
236+
return dedent(
237+
"""\
238+
name: MyLib
239+
240+
dependencies:
241+
"""
242+
) + "".join(f" - {dep}\n" for dep in deps)
243+
234244
def format_deps(
235245
filename: str, all_deps: Union[Deps, Tuple[Deps, ExtraDeps]]
236246
) -> str:
@@ -244,6 +254,7 @@ def format_deps(
244254
"setup.cfg": format_setup_cfg,
245255
"pyproject.toml": format_pyproject_toml,
246256
"pixi.toml": format_pixi_toml,
257+
"environment.yml": format_environment_yml,
247258
}
248259
formatter = formatters.get(Path(filename).name, format_requirements_txt)
249260
return formatter(deps, extras)

tests/test_cmdline.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ def test_list_sources__in_varied_project__lists_all_files(fake_project):
495495
"pyproject.toml": ["foo"],
496496
"setup.py": ["foo"],
497497
"setup.cfg": ["foo"],
498+
"environment.yml": ["foo"],
498499
},
499500
fake_venvs={"my_venv": {}},
500501
)
@@ -512,6 +513,7 @@ def test_list_sources__in_varied_project__lists_all_files(fake_project):
512513
"pyproject.toml",
513514
"setup.py",
514515
"setup.cfg",
516+
"environment.yml",
515517
str(_site_packages),
516518
]
517519
]

tests/test_cmdline_options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"setup.cfg",
5757
"pyproject.toml",
5858
"pixi.toml",
59+
"environment.yml",
5960
}
6061
example_python_stdin = dedent(
6162
"""\

tests/test_deps_parser_determination.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,31 @@
3030
("setup.cfg", ParserChoice.SETUP_CFG),
3131
("pyproject.toml", ParserChoice.PYPROJECT_TOML),
3232
("pixi.toml", ParserChoice.PIXI_TOML),
33+
("environment.yml", ParserChoice.ENVIRONMENT_YML),
3334
("anything_else", None),
3435
# in subdir:
3536
(str(Path("sub", "requirements.txt")), ParserChoice.REQUIREMENTS_TXT),
3637
(str(Path("sub", "setup.py")), ParserChoice.SETUP_PY),
3738
(str(Path("sub", "setup.cfg")), ParserChoice.SETUP_CFG),
3839
(str(Path("sub", "pyproject.toml")), ParserChoice.PYPROJECT_TOML),
3940
(str(Path("sub", "pixi.toml")), ParserChoice.PIXI_TOML),
41+
(str(Path("sub", "environment.yml")), ParserChoice.ENVIRONMENT_YML),
4042
(str(Path("sub", "anything_else")), None),
4143
# TODO: Make these absolute paths?
4244
(str(Path("abs", "requirements.txt")), ParserChoice.REQUIREMENTS_TXT),
4345
(str(Path("abs", "setup.py")), ParserChoice.SETUP_PY),
4446
(str(Path("abs", "setup.cfg")), ParserChoice.SETUP_CFG),
4547
(str(Path("abs", "pyproject.toml")), ParserChoice.PYPROJECT_TOML),
4648
(str(Path("abs", "pixi.toml")), ParserChoice.PIXI_TOML),
49+
(str(Path("abs", "environment.yml")), ParserChoice.ENVIRONMENT_YML),
4750
(str(Path("abs", "anything_else")), None),
4851
# using dep file name as a directory name is not supported:
4952
(str(Path("requirements.txt", "wat")), None),
5053
(str(Path("setup.py", "wat")), None),
5154
(str(Path("setup.cfg", "wat")), None),
5255
(str(Path("pyproject.toml", "wat")), None),
5356
(str(Path("pixi.toml", "wat")), None),
57+
(str(Path("environment.yml", "wat")), None),
5458
# variations that all map to requirements.txt parser;
5559
("requirements-dev.txt", ParserChoice.REQUIREMENTS_TXT),
5660
("test-requirements.txt", ParserChoice.REQUIREMENTS_TXT),
@@ -72,6 +76,7 @@ def test_first_applicable_parser(path, expect_choice):
7276
ParserChoice.SETUP_CFG: "setup.cfg",
7377
ParserChoice.PYPROJECT_TOML: "pyproject.toml",
7478
ParserChoice.PIXI_TOML: "pixi.toml",
79+
ParserChoice.ENVIRONMENT_YML: "environment.yml",
7580
}
7681
PARSER_CHOICE_FILE_NAME_MISMATCH_GRID = {
7782
pc: [fn for _pc, fn in PARSER_CHOICE_FILE_NAME_MATCH_GRID.items() if pc != _pc]

tests/test_extract_deps_success.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,21 @@ def test_find_and_parse_sources__project_with_pixi_toml__returns_list(fake_proje
721721
assert_unordered_equivalence(actual, expect)
722722

723723

724+
def test_find_and_parse_sources__project_with_environment_yml__returns_list(
725+
fake_project,
726+
):
727+
tmp_path = fake_project(
728+
files_with_declared_deps={
729+
"environment.yml": ["numpy", "pandas"], # dependencies
730+
},
731+
)
732+
expect = ["numpy", "pandas"]
733+
settings = Settings(code=set(), deps={tmp_path})
734+
deps_sources = list(find_sources(settings, {DepsSource}))
735+
actual = collect_dep_names(parse_sources(deps_sources))
736+
assert_unordered_equivalence(actual, expect)
737+
738+
724739
def test_find_and_parse_sources__project_with_setup_cfg__returns_list(fake_project):
725740
tmp_path = fake_project(
726741
files_with_declared_deps={

0 commit comments

Comments
 (0)