diff --git a/dependente/cli.py b/dependente/cli.py index 534ca1a..e8dddd8 100644 --- a/dependente/cli.py +++ b/dependente/cli.py @@ -8,16 +8,12 @@ """ import sys import traceback +from pathlib import Path import click from .converters import pin_to_oldest -from .parsers import ( - parse_requirements, - parse_sources, - read_pyproject_toml, - read_setup_cfg, -) +from .parsers import get_parser, validate_sources @click.command(context_settings={"help_option_names": ["-h", "--help"]}) @@ -54,28 +50,30 @@ def main(source, oldest, verbose): Supported formats: - * pyproject.toml (only build-system > requires) + * pyproject.toml (build-system > requires, project > dependencies and + project.optional-dependencies) * setup.cfg (install_requires and options.extras_require) """ reporter = Reporter(verbose) - readers = {"setup.cfg": read_setup_cfg, "pyproject.toml": read_pyproject_toml} - reporter.echo(f"Extracting dependencies: {source}") + sources = source.split(",") + validate_sources(sources) try: - sources = parse_sources(source) + config_files = get_sources_and_config_files(sources) + # Parse dependencies dependencies = [] - for config_file in sources: - if not sources[config_file]: - continue + for config_file, sources in config_files.items(): reporter.echo(f"Parsing {config_file}") - config = readers[config_file]() - dependencies_found = parse_requirements(config, sources[config_file]) + parser = get_parser(config_file) + dependencies_found = parser.parse_requirements(sources) reporter.echo(f" - {count(dependencies_found)} dependencies found") dependencies.extend(dependencies_found) + # Pin to oldest versions if oldest: reporter.echo("Pinning dependencies to their oldest versions") dependencies = pin_to_oldest(dependencies) + # Print gathered dependencies to stdout reporter.echo( f"Printing {count(dependencies)} dependencies to standard output", ) @@ -89,6 +87,55 @@ def main(source, oldest, verbose): sys.exit(1) +def get_sources_and_config_files(sources): + """ + Get configuration files in working directory and corresponding sources + + Find configuration files in current directory and sort out which sources + should be parsed from which config file. + + Parameters + ---------- + sources : list of str + List containing a subset of valid sources ("build", "install", + "extras"). + + Returns + ------- + config_files : dict + Dictionary with config files as keys. Their values are a list of + sources that should be parsed from each one of them. + + Raises + ------ + FileNotFoundError + If both ``setup.cfg`` and ``pyproject.toml`` are missing in the current + directory. + If "build" is in ``sources``, but ``pyproject.toml`` is not present in + the current directory. + """ + # Get configuration files in working directory + fnames = ("pyproject.toml", "setup.cfg") + config_fnames = [fname for fname in fnames if Path(fname).is_file()] + if not config_fnames: + raise FileNotFoundError("Missing 'pyproject.toml' and 'setup.cfg' files.") + # Sort out sources + if "build" in sources and "pyproject.toml" not in config_fnames: + raise FileNotFoundError( + "Missing 'pyproject.toml' file while asking for 'build' sources. " + "The 'build' sources can only be parsed from a 'pyproject.toml' file." + ) + if "setup.cfg" in config_fnames: + config_files = { + "pyproject.toml": ["build"] if "build" in sources else [], + "setup.cfg": [s for s in sources if s != "build"], + } + else: + config_files = {"pyproject.toml": sources} + config_files = {key: value for key, value in config_files.items() if value} + return config_files + + def count(dependencies): """ Count the number of dependencies in a list. diff --git a/dependente/parsers.py b/dependente/parsers.py index 506f5c9..4ac03a6 100644 --- a/dependente/parsers.py +++ b/dependente/parsers.py @@ -5,127 +5,209 @@ Functions for extracting the dependency information from files. """ import configparser +from pathlib import Path import tomli -def parse_sources(source): +def get_parser(fname): """ - Parse the input string for sources and separate based on config file type. - - Parameters - ---------- - source : str - Input source specification. - - Returns - ------- - sources : dict - Dictionary of parse sources. Keys are config file names. + Return instance of a Parser class based on the type of config file. """ - sources = {"setup.cfg": [], "pyproject.toml": []} - valid_sources = { - "install": {"file": "setup.cfg", "option": "install_requires"}, - "extras": {"file": "setup.cfg", "option": "options.extras_require"}, - "build": {"file": "pyproject.toml", "option": "build-system"}, - } - for entry in sorted(source.strip().split(",")): - if entry not in valid_sources: - raise ValueError( - f"Invalid source '{entry}'. Must be one of {list(valid_sources.keys())}." - ) - sources[valid_sources[entry]["file"]].append(valid_sources[entry]["option"]) - return sources + fname = Path(fname) + if fname.suffix == ".cfg": + parser = ParserSetupCfg(fname) + elif fname.suffix == ".toml": + parser = ParserPyprojectToml(fname) + else: + raise ValueError( + f"Invalid configuration file '{fname}' with suffix '{fname.suffix}'. " + "Only '.cfg' and '.toml' are supported." + ) + return parser -def read_setup_cfg(fname="setup.cfg"): +def validate_sources(sources): """ - Read the setup.cfg file into a dictionary. - """ - config = configparser.ConfigParser() - # Use read_file to get a FileNotFoundError if setup.cfg is missing. Using - # read returns an empty config instead of raising an exception. - with open(fname, "rt") as config_source: - config.read_file(config_source) - config_dict = { - section: dict((key, value.strip()) for key, value in config.items(section)) - for section in config.sections() - } - return config_dict - - -def read_pyproject_toml(fname="pyproject.toml"): - """ - Read the pyproject.toml file into a dictionary. - """ - with open(fname, "rb") as config_source: - config = tomli.load(config_source) - return config - + Validate sources -def parse_requirements(config, sources): - """ - Parse the sources from setup.cfg and pyproject.toml. + Check if sources is form by a subset of "build", "install" and "extras". Parameters ---------- - config : dict - The configuration file read into a dictionary. sources : list - List of section names from the config file. + List of sources. Valid sources are "build", "install" and "extras". + """ + if not sources: + raise ValueError( + "No sources were provided. " + "Please choose a subset of 'build', 'install' and 'extras'." + ) + valid_sources = set(["build", "install", "extras"]) + if not set(sources).issubset(valid_sources): + invalid = valid_sources - set(sources) + raise ValueError( + f"Invalid sources '{invalid}'. " f"Choose a subset of '{valid_sources}'." + ) + repeated_sources = [s for s in sources if sources.count(s) > 1] + if repeated_sources: + raise ValueError(f"Found repeated sources: '{repeated_sources}'.") - Returns - ------- - dependencies : list - List of dependencies read from the config file. Includes some comments. +class ParserSetupCfg: + """ + Parser for setup.cfg files """ - readers = { - "install_requires": get_setup_cfg_install, - "options.extras_require": get_setup_cfg_extras, - "build-system": get_pyproject_toml_build, - } - requirements = [] - for source in sources: - requirements.extend(readers[source](config)) - return requirements - - -def get_setup_cfg_install(config): - "Extract the install requirements from the configuration" - source = "install_requires" - if source not in config["options"]: - raise ValueError(f"Missing '{source}' field in setup.cfg.") - requirements = ["# Install (run-time) dependencies from setup.cfg"] - for package in config["options"][source].strip().split("\n"): - requirements.append(package.strip()) - return requirements - - -def get_setup_cfg_extras(config): - "Extract the extra requirements from the configuration" - source = "options.extras_require" - if source not in config: - raise ValueError(f"Missing '{source}' section in setup.cfg.") - requirements = ["# Extra (optional) dependencies from setup.cfg"] - for section in config[source]: - requirements.append(f"# extra: {section}") - for package in config[source][section].strip().split("\n"): - requirements.append(package.strip()) - return requirements + def __init__(self, fname): + fname = Path(fname) + self.fname = fname + + @property + def config(self): + """ + Return content of the setup.cfg file into a dictionary + """ + if not hasattr(self, "_config"): + config = configparser.ConfigParser() + # Use read_file to get a FileNotFoundError if setup.cfg is missing. + # Using read returns an empty config instead of raising an + # exception. + with open(self.fname, "rt") as config_source: + config.read_file(config_source) + self._config = { + section: dict( + (key, value.strip()) for key, value in config.items(section) + ) + for section in config.sections() + } + return self._config + + def parse_requirements(self, sources): + """ + Parse requirements from setup.cfg config file + """ + validate_sources(sources) + dependencies = [] + if "build" in sources: + raise ValueError("Cannot parse 'build' sources from setup.cfg.") + if "install" in sources: + dependencies += self.parse_install_dependencies() + if "extras" in sources: + dependencies += self.parse_extra_dependencies() + return dependencies + + def parse_install_dependencies(self): + """ + Parse install requirements from setup.cfg config file + """ + source = "install_requires" + if source not in self.config["options"]: + raise ValueError(f"Missing '{source}' field in setup.cfg.") + packages = [ + package.strip() + for package in self.config["options"][source].strip().split("\n") + ] + requirements = ["# Install (run-time) dependencies from setup.cfg"] + packages + return requirements + + def parse_extra_dependencies(self): + """ + Parse extra requirements from setup.cfg config file + """ + source = "options.extras_require" + if source not in self.config: + raise ValueError(f"Missing '{source}' section in setup.cfg.") + requirements = ["# Extra (optional) dependencies from setup.cfg"] + for section in self.config[source]: + requirements.append(f"# extra: {section}") + for package in self.config[source][section].strip().split("\n"): + requirements.append(package.strip()) + return requirements + + +class ParserPyprojectToml: + """ + Parser for pyproject.toml files + """ -def get_pyproject_toml_build(config): - "Extract the build requirements from the configuration" - source = "build-system" - if source not in config: - raise ValueError(f"Missing '{source}' section in pyproject.toml.") - if "requires" not in config[source]: - raise ValueError( - f"Missing 'requires' entry from the '{source}' section in " - "pyproject.toml." - ) - requirements = ["# Build dependencies from pyproject.toml"] - for package in config[source]["requires"]: - requirements.append(package.strip()) - return requirements + def __init__(self, fname): + fname = Path(fname) + self.fname = fname + + @property + def config(self): + """ + Return content of the pyproject.toml file into a dictionary + """ + if not hasattr(self, "_config"): + with open(self.fname, "rb") as config_source: + self._config = tomli.load(config_source) + return self._config + + def parse_requirements(self, sources): + """ + Parse requirements from setup.cfg config file + """ + validate_sources(sources) + dependencies = [] + if "build" in sources: + dependencies += self.parse_build_dependencies() + if "install" in sources: + dependencies += self.parse_install_dependencies() + if "extras" in sources: + dependencies += self.parse_extra_dependencies() + return dependencies + + def parse_build_dependencies(self): + """ + Parse build requirements from setup.cfg config file + """ + source = "build-system" + if source not in self.config: + raise ValueError(f"Missing '{source}' section in pyproject.toml.") + if "requires" not in self.config[source]: + raise ValueError( + f"Missing 'requires' entry from the '{source}' section in " + "pyproject.toml." + ) + requirements = ["# Build dependencies from pyproject.toml"] + for package in self.config[source]["requires"]: + requirements.append(package.strip()) + return requirements + + def parse_install_dependencies(self): + """ + Parse install requirements from setup.cfg config file + """ + source = "project" + if source not in self.config: + raise ValueError(f"Missing '{source}' section in pyproject.toml.") + if "dependencies" not in self.config[source]: + raise ValueError( + f"Missing 'dependencies' entry from the '{source}' section in " + "pyproject.toml." + ) + requirements = ["# Install dependencies from pyproject.toml"] + for package in self.config[source]["dependencies"]: + requirements.append(package.strip().replace(" ", "")) + return requirements + + def parse_extra_dependencies(self): + """ + Parse extra requirements from setup.cfg config file + """ + source = "project" + subsource = "optional-dependencies" + if source not in self.config: + raise ValueError(f"Missing '{source}' section in pyproject.toml.") + if subsource not in self.config[source]: + raise ValueError( + f"Missing '{source}.{subsource}' section in pyproject.toml." + ) + requirements = ["# Extra (optional) dependencies from pyproject.toml"] + for section, packages in self.config[source][subsource].items(): + requirements.append(f"# extra: {section}") + for package in packages: + requirements.append(package.strip().replace(" ", "")) + return requirements diff --git a/dependente/tests/conftest.py b/dependente/tests/conftest.py index f7c0d43..634257e 100644 --- a/dependente/tests/conftest.py +++ b/dependente/tests/conftest.py @@ -11,10 +11,16 @@ @pytest.fixture() def setup_cfg(): - "The text contents of the test file." + "The path to the sample setup.cfg test file." return str(Path(__file__).parent / "data" / "sample_setup.cfg") +@pytest.fixture() +def pyproject_toml(): + "The path to the sample pyproject.toml test file." + return str(Path(__file__).parent / "data" / "sample_pyproject.toml") + + @pytest.fixture() def setup_cfg_config(): "The parsed contents of the file." @@ -31,7 +37,34 @@ def setup_cfg_config(): @pytest.fixture() -def setup_cfg_install(): +def pyproject_toml_config(): + "The parsed contents of the file." + contents = { + "build-system": { + "requires": [ + "setuptools>=45", + "setuptools_scm>=6.2", + "wheel", + ] + }, + "project": { + "dependencies": [ + "click >= 8.0.0, < 9.0.0", + "rich >= 9.6.0, < 11.0.0", + "tomli >= 1.0.0, < 3.0.0", + ], + "optional-dependencies": { + "jupyter": [ + "nbformat >= 5.1", + ], + }, + }, + } + return contents + + +@pytest.fixture() +def install_dependencies(): "The install requirements" contents = [ "click>=8.0.0,<9.0.0", @@ -42,8 +75,8 @@ def setup_cfg_install(): @pytest.fixture() -def setup_cfg_extras(): - "The extra requirements" +def extras_dependencies(): + "The extras requirements" contents = [ "nbformat>=5.1", ] @@ -51,28 +84,7 @@ def setup_cfg_extras(): @pytest.fixture() -def pyproject_toml(): - "The text contents of the test file." - return str(Path(__file__).parent / "data" / "sample_pyproject.toml") - - -@pytest.fixture() -def pyproject_toml_config(): - "The parsed contents of the file." - contents = { - "build-system": { - "requires": [ - "setuptools>=45", - "setuptools_scm>=6.2", - "wheel", - ] - }, - } - return contents - - -@pytest.fixture() -def pyproject_toml_build(): +def build_dependencies(): "The build requirements" contents = [ "setuptools>=45", diff --git a/dependente/tests/data/sample_pyproject.toml b/dependente/tests/data/sample_pyproject.toml index 3f0ad82..02c5913 100644 --- a/dependente/tests/data/sample_pyproject.toml +++ b/dependente/tests/data/sample_pyproject.toml @@ -1,2 +1,12 @@ [build-system] requires = ["setuptools>=45", "setuptools_scm>=6.2", "wheel"] + +[project] +dependencies = [ + "click >= 8.0.0, < 9.0.0", + "rich >= 9.6.0, < 11.0.0", + "tomli >= 1.0.0, < 3.0.0", +] + +[project.optional-dependencies] +jupyter = ["nbformat >= 5.1",] diff --git a/dependente/tests/test_cli_utils.py b/dependente/tests/test_cli_utils.py new file mode 100644 index 0000000..8d6426a --- /dev/null +++ b/dependente/tests/test_cli_utils.py @@ -0,0 +1,150 @@ +""" +Test utility functions from cli.py +""" +import os +import shutil + +import pytest + +from ..cli import get_sources_and_config_files + + +class TestGetSourcesAndConfigFiles: + """ + Test if get_sources_and_config_files() work as expected when only setup.cfg + """ + + @pytest.mark.parametrize( + "sources", + ( + ["install"], + ["extras"], + ["build"], + ), + ) + def test_files_missing(self, sources, tmp_path): + """ + Test if error is raised when no configuration files are found in cwd + """ + # Cd to tmp_path + os.chdir(tmp_path) + # Check if error is raised + msg = "Missing 'pyproject.toml' and 'setup.cfg' files." + with pytest.raises(FileNotFoundError, match=msg): + get_sources_and_config_files(sources) + + @pytest.mark.parametrize( + "sources", + ( + ["install"], + ["extras"], + ["install", "extras"], + ), + ) + def test_setup_cfg(self, sources, setup_cfg, tmp_path): + """ + Test when only setup.cfg exists + """ + # Copy sample file to tmp_path and cd to tmp_path + shutil.copy(src=setup_cfg, dst=tmp_path / "setup.cfg") + os.chdir(tmp_path) + # Check results with expected outcome + result = get_sources_and_config_files(sources) + expected = {"setup.cfg": sources} + assert result == expected + + @pytest.mark.parametrize( + "sources", + ( + ["build"], + ["install", "build"], + ["extras", "build"], + ["install", "build", "extras"], + ), + ) + def test_setup_with_build(self, sources, setup_cfg, tmp_path): + """ + Test if error is raised when only setup.cfg exists and "build" is in + sources. + """ + # Copy sample file to tmp_path and cd to tmp_path + shutil.copy(src=setup_cfg, dst=tmp_path / "setup.cfg") + os.chdir(tmp_path) + # Test if error is raised + msg = "Missing 'pyproject.toml' file while asking for 'build' sources." + with pytest.raises(FileNotFoundError, match=msg): + get_sources_and_config_files(sources) + + @pytest.mark.parametrize( + "sources", + ( + ["build"], + ["install"], + ["extras"], + ["install", "extras"], + ["build", "install"], + ["build", "extras"], + ["build", "install", "extras"], + ), + ) + def test_pyproject_toml(self, sources, pyproject_toml, tmp_path): + """ + Test when only pyproject.toml exists + """ + # Copy sample file to tmp_path and cd to tmp_path + shutil.copy(src=pyproject_toml, dst=tmp_path / "pyproject.toml") + os.chdir(tmp_path) + # Check results with expected outcome + result = get_sources_and_config_files(sources) + expected = {"pyproject.toml": sources} + assert result == expected + + @pytest.mark.parametrize( + "sources", + ( + ["install"], + ["extras"], + ["install", "extras"], + ), + ) + def test_setup_cfg_and_pyproject_toml_no_build( + self, sources, setup_cfg, pyproject_toml, tmp_path + ): + """ + Test when setup.cfg and pyproject.toml exist and build is not in + sources. + """ + # Copy sample files to tmp_path and cd to tmp_path + shutil.copy(src=setup_cfg, dst=tmp_path / "setup.cfg") + shutil.copy(src=pyproject_toml, dst=tmp_path / "pyproject.toml") + os.chdir(tmp_path) + # Check results with expected outcome + result = get_sources_and_config_files(sources) + expected = {"setup.cfg": sources} + assert result == expected + + @pytest.mark.parametrize( + "sources", + ( + ["build"], + ["build", "install"], + ["build", "extras"], + ["build", "install", "extras"], + ), + ) + def test_setup_cfg_and_pyproject_toml( + self, sources, setup_cfg, pyproject_toml, tmp_path + ): + """ + Test when setup.cfg and pyproject.toml exist with build in sources + """ + # Copy sample files to tmp_path and cd to tmp_path + shutil.copy(src=setup_cfg, dst=tmp_path / "setup.cfg") + shutil.copy(src=pyproject_toml, dst=tmp_path / "pyproject.toml") + os.chdir(tmp_path) + # Check results with expected outcome + result = get_sources_and_config_files(sources) + expected = {"pyproject.toml": ["build"]} + if sources != ["build"]: + expected["setup.cfg"] = [s for s in sources if s != "build"] + assert result == expected diff --git a/dependente/tests/test_parsers.py b/dependente/tests/test_parsers.py index 59dfee5..f422625 100644 --- a/dependente/tests/test_parsers.py +++ b/dependente/tests/test_parsers.py @@ -8,133 +8,208 @@ """ import pytest -from ..parsers import ( - get_pyproject_toml_build, - get_setup_cfg_extras, - get_setup_cfg_install, - parse_requirements, - parse_sources, - read_pyproject_toml, - read_setup_cfg, -) +from ..parsers import ParserPyprojectToml, ParserSetupCfg, get_parser, validate_sources @pytest.mark.parametrize( - "source,expected", + "fname, expected_class", [ - ("install", {"setup.cfg": ["install_requires"], "pyproject.toml": []}), - ("build", {"setup.cfg": [], "pyproject.toml": ["build-system"]}), - ("extras", {"setup.cfg": ["options.extras_require"], "pyproject.toml": []}), + ("setup.cfg", ParserSetupCfg), + ("pyproject.toml", ParserPyprojectToml), + ("invalid.file", None), + ], + ids=["setup.cfg", "pyproject.toml", "invalid.file"], +) +def test_get_parser(fname, expected_class): + """ + Test get_parser function + """ + if expected_class is not None: + assert isinstance(get_parser(fname), expected_class) + else: + with pytest.raises(ValueError, match="Invalid configuration file"): + get_parser(fname) + + +class TestValidateSources: + """ + Test validate_sources function + """ + + @pytest.mark.parametrize( + "sources", ( - "install,extras", - { - "setup.cfg": ["options.extras_require", "install_requires"], - "pyproject.toml": [], - }, + ["build"], + ["install"], + ["extras"], + ["build", "install", "extras"], + ["extras", "build", "install"], ), + ) + def test_valid_sources(self, sources): + """ + Test if the function don't raises errors after valid sources + """ + validate_sources(sources) + + @pytest.mark.parametrize( + "sources", (["invalid"], ["build", "extras", "invalid"], []) + ) + def test_invalid_sources(self, sources): + """ + Test if the function raises errors after invalid sources + """ + if sources: + msg = "Invalid sources" + else: + msg = "No sources were provided" + with pytest.raises(ValueError, match=msg): + validate_sources(sources) + + def test_repeated_sources(self): + """ + Test if the function raises errors after repeated sources + """ + sources = ["build", "extras", "build"] + with pytest.raises(ValueError, match="Found repeated sources"): + validate_sources(sources) + + +class TestSetupCfgParser: + """ + Test parser class for setup.cfg files. + """ + + def test_config(self, setup_cfg, setup_cfg_config): + """ + Test if config was correctly read from file + """ + parser = ParserSetupCfg(setup_cfg) + assert parser.config == setup_cfg_config + + @pytest.mark.parametrize( + "sources", ( - "extras,build", - { - "setup.cfg": ["options.extras_require"], - "pyproject.toml": ["build-system"], - }, + ["build"], + ["build", "install"], + ["build", "install", "extras"], + ["install", "extras", "build"], ), + ) + def test_parse_build(self, setup_cfg, sources): + """Test if error is raised when "build" requirements are passed.""" + parser = ParserSetupCfg(setup_cfg) + msg = "Cannot parse 'build' sources from setup.cfg." + with pytest.raises(ValueError, match=msg): + parser.parse_requirements(sources) + + def test_parse_install(self, setup_cfg, install_dependencies): + """Test if parsed install requirements are correct.""" + parser = ParserSetupCfg(setup_cfg) + parsed = [ + line + for line in parser.parse_requirements(["install"]) + if not line.startswith("#") + ] + assert install_dependencies == parsed + + def test_parse_extras(self, setup_cfg, extras_dependencies): + """Test if parsed extras dependencies are correct.""" + parser = ParserSetupCfg(setup_cfg) + parsed = [ + line + for line in parser.parse_requirements(["extras"]) + if not line.startswith("#") + ] + assert extras_dependencies == parsed + + @pytest.mark.parametrize("sources", (["install", "extras"], ["extras", "install"])) + def test_parse_multiple( + self, sources, setup_cfg, install_dependencies, extras_dependencies + ): + """Test if parsing multiple sources works as expected.""" + parser = ParserSetupCfg(setup_cfg) + parsed = [ + line + for line in parser.parse_requirements(sources) + if not line.startswith("#") + ] + assert install_dependencies + extras_dependencies == parsed + + +class TestPyprojectTomlParser: + """ + Test parser class for pyproject.toml files. + """ + + def test_config(self, pyproject_toml, pyproject_toml_config): + """ + Test if config was correctly read from file + """ + parser = ParserPyprojectToml(pyproject_toml) + assert parser.config == pyproject_toml_config + + def test_parse_build(self, pyproject_toml, build_dependencies): + """Test if error is raised when "build" requirements are passed.""" + parser = ParserPyprojectToml(pyproject_toml) + parsed = [ + line + for line in parser.parse_requirements(["build"]) + if not line.startswith("#") + ] + assert build_dependencies == parsed + + def test_parse_install(self, pyproject_toml, install_dependencies): + """Test if parsed install requirements are correct.""" + parser = ParserPyprojectToml(pyproject_toml) + parsed = [ + line + for line in parser.parse_requirements(["install"]) + if not line.startswith("#") + ] + assert install_dependencies == parsed + + def test_parse_extras(self, pyproject_toml, extras_dependencies): + """Test if parsed extras dependencies are correct.""" + parser = ParserPyprojectToml(pyproject_toml) + parsed = [ + line + for line in parser.parse_requirements(["extras"]) + if not line.startswith("#") + ] + assert extras_dependencies == parsed + + @pytest.mark.parametrize( + "sources", ( - "build,extras,install", - { - "setup.cfg": ["options.extras_require", "install_requires"], - "pyproject.toml": ["build-system"], - }, + ["build", "install"], + ["build", "extras"], + ["install", "extras"], + ["extras", "install"], + ["build", "install", "extras"], + ["extras", "install", "build"], ), - ], - ids=["install", "build", "extras", "install,extras", "extras,build", "all"], -) -def test_parse_sources(source, expected): - "Check the parsing of input data sources" - assert expected == parse_sources(source) - - -def test_parse_sources_invalid(): - "Should raise an exception on invalid input" - with pytest.raises(ValueError) as error: - parse_sources("something") - assert "'something'" in str(error) - - -def test_read_setup_cfg(setup_cfg, setup_cfg_config): - "Check that setup.cfg is read properly" - assert setup_cfg_config == read_setup_cfg(setup_cfg) - - -def test_read_pyproject_toml(pyproject_toml, pyproject_toml_config): - "Check that pyproject.toml is read properly" - assert pyproject_toml_config == read_pyproject_toml(pyproject_toml) - - -def test_parse_requirements_install(setup_cfg_config, setup_cfg_install): - "Check that setup.cfg parses install requirements properly" - parsed = [ - line - for line in parse_requirements(setup_cfg_config, ["install_requires"]) - if not line.startswith("#") - ] - assert setup_cfg_install == parsed - - -def test_parse_requirements_extras(setup_cfg_config, setup_cfg_extras): - "Check that setup.cfg parses extra requirements properly" - parsed = [ - line - for line in parse_requirements(setup_cfg_config, ["options.extras_require"]) - if not line.startswith("#") - ] - assert setup_cfg_extras == parsed - - -def test_parse_requirements_build(pyproject_toml_config, pyproject_toml_build): - "Check that pyproject.toml parses all requirements properly" - parsed = [ - line - for line in parse_requirements(pyproject_toml_config, ["build-system"]) - if not line.startswith("#") - ] - assert pyproject_toml_build == parsed - - -def test_parse_requirements_multiple( - setup_cfg_config, setup_cfg_extras, setup_cfg_install -): - "Check that setup.cfg parses all requirements properly" - parsed = [ - line - for line in parse_requirements( - setup_cfg_config, ["options.extras_require", "install_requires"] - ) - if not line.startswith("#") - ] - expected = setup_cfg_extras + setup_cfg_install - assert expected == parsed - - -def test_parse_requirements_install_fail(): - "Check that parsing fails with an exception" - with pytest.raises(ValueError) as error: - get_setup_cfg_install({"options": {"something": []}}) - assert "Missing 'install_requires'" in str(error) - - -def test_parse_requirements_extras_fail(): - "Check that parsing fails with an exception" - with pytest.raises(ValueError) as error: - get_setup_cfg_extras({"options": {"something": []}}) - assert "Missing 'options.extras_require'" in str(error) - - -def test_parse_requirements_build_fail(): - "Check that parsing fails with an exception" - with pytest.raises(ValueError) as error: - get_pyproject_toml_build({"meh": ["something"]}) - assert "Missing 'build-system'" in str(error) - with pytest.raises(ValueError) as error: - get_pyproject_toml_build({"build-system": {"something": []}}) - assert "Missing 'requires'" in str(error) + ) + def test_parse_multiple( + self, + sources, + pyproject_toml, + install_dependencies, + extras_dependencies, + build_dependencies, + ): + """Test if parsing multiple sources works as expected.""" + parser = ParserPyprojectToml(pyproject_toml) + parsed = [ + line + for line in parser.parse_requirements(sources) + if not line.startswith("#") + ] + expected = [] + if "build" in sources: + expected += build_dependencies + if "install" in sources: + expected += install_dependencies + if "extras" in sources: + expected += extras_dependencies + assert expected == parsed