Skip to content

Commit

Permalink
Add environment.yml support to the rest of FawltyDeps
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jherland committed Sep 19, 2024
1 parent 0ca230f commit 10a3374
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 3 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ dependencies. A number of file formats are supported:
`extras_require` arguments)
- `setup.cfg`
- `pixi.toml`
- `environment.yml`

The `--deps` option accepts a space-separated list of files or directories.
Each file will be parsed for declared dependencies; each directory will
Expand Down Expand Up @@ -437,8 +438,8 @@ Here is a complete list of configuration directives we support:
in the repository.
- `deps_parser_choice`: Manually select which format to use for parsing
declared dependencies. Must be one of `"requirements.txt"`, `"setup.py"`,
`"setup.cfg"`, `"pyproject.toml"`, `"pixi.toml"`, or leave it unset
(i.e. the default) for auto-detection (based on filename).
`"setup.cfg"`, `"pyproject.toml"`, `"pixi.toml"`, `"environment.yml"`, or
leave it unset (i.e. the default) for auto-detection (based on filename).
- `install-deps`: Automatically install Python dependencies gathered with
FawltyDeps into a temporary virtual environment. This will use `uv` or `pip`
to download and install packages from PyPI by default.
Expand Down
4 changes: 4 additions & 0 deletions fawltydeps/extract_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import Callable, Iterable, Iterator, NamedTuple, Optional

from fawltydeps.extract_deps_environment_yml import parse_environment_yml
from fawltydeps.extract_deps_pixi import parse_pixi_toml
from fawltydeps.extract_deps_pyproject import parse_pyproject_toml
from fawltydeps.extract_deps_requirements import parse_requirements_txt
Expand Down Expand Up @@ -53,6 +54,9 @@ def first_applicable_parser(path: Path) -> Optional[ParserChoice]:
ParserChoice.PIXI_TOML: ParsingStrategy(
lambda path: path.name == "pixi.toml", parse_pixi_toml
),
ParserChoice.ENVIRONMENT_YML: ParsingStrategy(
lambda path: path.name == "environment.yml", parse_environment_yml
),
}


Expand Down
1 change: 1 addition & 0 deletions fawltydeps/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ParserChoice(Enum):
SETUP_CFG = "setup.cfg"
PYPROJECT_TOML = "pyproject.toml"
PIXI_TOML = "pixi.toml"
ENVIRONMENT_YML = "environment.yml"

def __str__(self) -> str:
return self.value
Expand Down
13 changes: 12 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def fake_project(write_tmp_files, fake_venv): # noqa: C901
lists of strings (extras/optional deps).
The dependencies will be written into associated files, formatted
according to the filenames (must be one of requirements.txt, setup.py,
setup.cfg, pyproject.toml, or pixi.toml).
setup.cfg, pyproject.toml, pixi.toml, or environment.yml).
- extra_file_contents: a dict with extra files and their associated contents
to be forwarded directly to write_tmp_files().
Expand Down Expand Up @@ -231,6 +231,16 @@ def format_pixi_toml(deps: Deps, extras: ExtraDeps) -> str:
ret += "\n".join(f'{dep} = "*"' for dep in deps)
return ret

def format_environment_yml(deps: Deps, no_extras: ExtraDeps) -> str:
assert not no_extras # not supported
return dedent(
"""\
name: MyLib
dependencies:
"""
) + "".join(f" - {dep}\n" for dep in deps)

def format_deps(
filename: str, all_deps: Union[Deps, Tuple[Deps, ExtraDeps]]
) -> str:
Expand All @@ -244,6 +254,7 @@ def format_deps(
"setup.cfg": format_setup_cfg,
"pyproject.toml": format_pyproject_toml,
"pixi.toml": format_pixi_toml,
"environment.yml": format_environment_yml,
}
formatter = formatters.get(Path(filename).name, format_requirements_txt)
return formatter(deps, extras)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ def test_list_sources__in_varied_project__lists_all_files(fake_project):
"pyproject.toml": ["foo"],
"setup.py": ["foo"],
"setup.cfg": ["foo"],
"environment.yml": ["foo"],
},
fake_venvs={"my_venv": {}},
)
Expand All @@ -512,6 +513,7 @@ def test_list_sources__in_varied_project__lists_all_files(fake_project):
"pyproject.toml",
"setup.py",
"setup.cfg",
"environment.yml",
str(_site_packages),
]
]
Expand Down
1 change: 1 addition & 0 deletions tests/test_cmdline_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"setup.cfg",
"pyproject.toml",
"pixi.toml",
"environment.yml",
}
example_python_stdin = dedent(
"""\
Expand Down
5 changes: 5 additions & 0 deletions tests/test_deps_parser_determination.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,31 @@
("setup.cfg", ParserChoice.SETUP_CFG),
("pyproject.toml", ParserChoice.PYPROJECT_TOML),
("pixi.toml", ParserChoice.PIXI_TOML),
("environment.yml", ParserChoice.ENVIRONMENT_YML),
("anything_else", None),
# in subdir:
(str(Path("sub", "requirements.txt")), ParserChoice.REQUIREMENTS_TXT),
(str(Path("sub", "setup.py")), ParserChoice.SETUP_PY),
(str(Path("sub", "setup.cfg")), ParserChoice.SETUP_CFG),
(str(Path("sub", "pyproject.toml")), ParserChoice.PYPROJECT_TOML),
(str(Path("sub", "pixi.toml")), ParserChoice.PIXI_TOML),
(str(Path("sub", "environment.yml")), ParserChoice.ENVIRONMENT_YML),
(str(Path("sub", "anything_else")), None),
# TODO: Make these absolute paths?
(str(Path("abs", "requirements.txt")), ParserChoice.REQUIREMENTS_TXT),
(str(Path("abs", "setup.py")), ParserChoice.SETUP_PY),
(str(Path("abs", "setup.cfg")), ParserChoice.SETUP_CFG),
(str(Path("abs", "pyproject.toml")), ParserChoice.PYPROJECT_TOML),
(str(Path("abs", "pixi.toml")), ParserChoice.PIXI_TOML),
(str(Path("abs", "environment.yml")), ParserChoice.ENVIRONMENT_YML),
(str(Path("abs", "anything_else")), None),
# using dep file name as a directory name is not supported:
(str(Path("requirements.txt", "wat")), None),
(str(Path("setup.py", "wat")), None),
(str(Path("setup.cfg", "wat")), None),
(str(Path("pyproject.toml", "wat")), None),
(str(Path("pixi.toml", "wat")), None),
(str(Path("environment.yml", "wat")), None),
# variations that all map to requirements.txt parser;
("requirements-dev.txt", ParserChoice.REQUIREMENTS_TXT),
("test-requirements.txt", ParserChoice.REQUIREMENTS_TXT),
Expand All @@ -72,6 +76,7 @@ def test_first_applicable_parser(path, expect_choice):
ParserChoice.SETUP_CFG: "setup.cfg",
ParserChoice.PYPROJECT_TOML: "pyproject.toml",
ParserChoice.PIXI_TOML: "pixi.toml",
ParserChoice.ENVIRONMENT_YML: "environment.yml",
}
PARSER_CHOICE_FILE_NAME_MISMATCH_GRID = {
pc: [fn for _pc, fn in PARSER_CHOICE_FILE_NAME_MATCH_GRID.items() if pc != _pc]
Expand Down
15 changes: 15 additions & 0 deletions tests/test_extract_deps_success.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,21 @@ def test_find_and_parse_sources__project_with_pixi_toml__returns_list(fake_proje
assert_unordered_equivalence(actual, expect)


def test_find_and_parse_sources__project_with_environment_yml__returns_list(
fake_project,
):
tmp_path = fake_project(
files_with_declared_deps={
"environment.yml": ["numpy", "pandas"], # dependencies
},
)
expect = ["numpy", "pandas"]
settings = Settings(code=set(), deps={tmp_path})
deps_sources = list(find_sources(settings, {DepsSource}))
actual = collect_dep_names(parse_sources(deps_sources))
assert_unordered_equivalence(actual, expect)


def test_find_and_parse_sources__project_with_setup_cfg__returns_list(fake_project):
tmp_path = fake_project(
files_with_declared_deps={
Expand Down

0 comments on commit 10a3374

Please sign in to comment.