def test_invalid_req(rule_runner: RuleRunner) -> None: """Test that we give a nice error message.""" fake_file_tgt = PythonRequirementsFile({"sources": ["doesnt matter"]}, Address("doesnt_matter")) with pytest.raises(ExecutionError) as exc: assert_python_requirements( rule_runner, "python_requirements()", "\n\nNot A Valid Req == 3.7", expected_file_dep=fake_file_tgt, expected_targets=[], ) assert "Invalid requirement 'Not A Valid Req == 3.7' in requirements.txt at line 3" in str( exc.value ) # Give a nice error message if it looks like they're using pip VCS-style requirements. with pytest.raises(ExecutionError) as exc: assert_python_requirements( rule_runner, "python_requirements()", "git+https://github.com/pypa/pip.git#egg=pip", expected_file_dep=fake_file_tgt, expected_targets=[], ) assert "It looks like you're trying to use a pip VCS-style requirement?" in str(exc.value)
def test_source_override(rule_runner: RuleRunner) -> None: assert_poetry_requirements( rule_runner, "poetry_requirements(source='subdir/pyproject.toml')", dedent("""\ [tool.poetry.dependencies] ansicolors = ">=1.18.0" [tool.poetry.dev-dependencies] """), pyproject_toml_relpath="subdir/pyproject.toml", expected_file_dep=PythonRequirementsFile( {"sources": ["subdir/pyproject.toml"]}, address=Address("", target_name="subdir_pyproject.toml"), ), expected_targets=[ PythonRequirementTarget( { "dependencies": [":subdir_pyproject.toml"], "requirements": [PipRequirement.parse("ansicolors>=1.18.0")], }, address=Address("", target_name="ansicolors"), ), ], )
def test_supply_python_requirements_file(rule_runner: RuleRunner) -> None: """This tests that we can supply our own `_python_requirements_file`.""" assert_pipenv_requirements( rule_runner, dedent( """ pipenv_requirements( requirements_relpath='custom/pipfile/Pipfile.lock', pipfile_target='//:custom_pipfile_target' ) _python_requirements_file( name='custom_pipfile_target', sources=['custom/pipfile/Pipfile.lock'] ) """ ), {"default": {"ansicolors": {"version": ">=1.18.0"}}}, expected_file_dep=PythonRequirementsFile( {"sources": ["custom/pipfile/Pipfile.lock"]}, address=Address("", target_name="custom_pipfile_target"), ), expected_targets=[ PythonRequirementLibrary( { "requirements": [Requirement.parse("ansicolors>=1.18.0")], "dependencies": ["//:custom_pipfile_target"], }, address=Address("", target_name="ansicolors"), ), ], pipfile_lock_relpath="custom/pipfile/Pipfile.lock", )
def test_requirements_txt(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement_library for each entry in a requirements.txt file. Some edge cases: * We ignore comments and options (values that start with `--`). * If a module_mapping is given, and the project is in the map, we copy over a subset of the mapping to the created target. * Projects get normalized thanks to Requirement.parse(). """ assert_python_requirements( rule_runner, "python_requirements(module_mapping={'ansicolors': ['colors']})", dedent( """\ # Comment. --find-links=https://duckduckgo.com ansicolors>=1.18.0 Django==3.2 ; python_version>'3' Un-Normalized-PROJECT # Inline comment. pip@ git+https://github.com/pypa/pip.git """ ), expected_file_dep=PythonRequirementsFile( {"sources": ["requirements.txt"]}, address=Address("", target_name="requirements.txt"), ), expected_targets=[ PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("ansicolors>=1.18.0")], "module_mapping": {"ansicolors": ["colors"]}, }, address=Address("", target_name="ansicolors"), ), PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("Django==3.2 ; python_version>'3'")], }, address=Address("", target_name="Django"), ), PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("Un_Normalized_PROJECT")], }, address=Address("", target_name="Un-Normalized-PROJECT"), ), PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("pip@ git+https://github.com/pypa/pip.git")], }, address=Address("", target_name="pip"), ), ], )
def test_invalid_req(self) -> None: """Test that we give a nice error message.""" with pytest.raises(ExecutionError) as exc: self.assert_python_requirements( "python_requirements()", "\n\nNot A Valid Req == 3.7", expected_file_dep=PythonRequirementsFile({}, address=Address("doesnt_matter")), expected_targets=[], ) assert ( "Invalid requirement in requirements.txt at line 3 due to value 'Not A Valid Req == " "3.7'." ) in str(exc.value)
def test_pipfile_lock(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement_library for each entry in a Pipfile.lock file. Edge cases: * Develop and Default requirements are used * If a module_mapping is given, and the project is in the map, we copy over a subset of the mapping to the created target. """ assert_pipenv_requirements( rule_runner, "pipenv_requirements(module_mapping={'ansicolors': ['colors']})", { "default": { "ansicolors": { "version": ">=1.18.0" } }, "develop": { "cachetools": { "markers": "python_version ~= '3.5'", "version": "==4.1.1" } }, }, expected_file_dep=PythonRequirementsFile( {"sources": ["Pipfile.lock"]}, Address("", target_name="Pipfile.lock")), expected_targets=[ PythonRequirementLibrary( { "requirements": [Requirement.parse("ansicolors>=1.18.0")], "dependencies": [":Pipfile.lock"], "module_mapping": { "ansicolors": ["colors"] }, }, Address("", target_name="ansicolors"), ), PythonRequirementLibrary( { "requirements": [ Requirement.parse( "cachetools==4.1.1;python_version ~= '3.5'") ], "dependencies": [":Pipfile.lock"], }, Address("", target_name="cachetools"), ), ], )
def test_no_tool_poetry(rule_runner: RuleRunner) -> None: with pytest.raises(ExecutionError) as exc: assert_poetry_requirements( rule_runner, "poetry_requirements()", """ foo = 4 """, expected_file_dep=PythonRequirementsFile( {"sources": ["pyproject.toml"]}, address=Address("", target_name="pyproject.toml"), ), expected_targets=[], ) assert "`tool.poetry` found in pyproject.toml" in str(exc.value)
def test_no_req_defined_warning(rule_runner: RuleRunner, caplog: Any) -> None: assert_poetry_requirements( rule_runner, "poetry_requirements()", """ [tool.poetry.dependencies] [tool.poetry.dev-dependencies] """, expected_file_dep=PythonRequirementsFile( {"sources": ["pyproject.toml"]}, address=Address("", target_name="pyproject.toml"), ), expected_targets=[], ) assert "No requirements defined" in caplog.text
def test_multiple_versions(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement_library for each unique dependency name in a requirements.txt file, grouping duplicated dependency names to handle multiple requirement strings per PEP 508.""" assert_python_requirements( rule_runner, "python_requirements()", dedent( """\ Django>=3.2 Django==3.2.7 confusedmonkey==86 repletewateringcan>=7 """ ), expected_file_dep=PythonRequirementsFile( {"sources": ["requirements.txt"]}, Address("", target_name="requirements.txt"), ), expected_targets=[ PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [ Requirement.parse("Django>=3.2"), Requirement.parse("Django==3.2.7"), ], }, Address("", target_name="Django"), ), PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("confusedmonkey==86")], }, Address("", target_name="confusedmonkey"), ), PythonRequirementLibrary( { "dependencies": [":requirements.txt"], "requirements": [Requirement.parse("repletewateringcan>=7")], }, Address("", target_name="repletewateringcan"), ), ], )
def test_bad_req_type(rule_runner: RuleRunner) -> None: with pytest.raises(ExecutionError) as exc: assert_poetry_requirements( rule_runner, "poetry_requirements()", """ [tool.poetry.dependencies] foo = 4 [tool.poetry.dev-dependencies] """, expected_file_dep=PythonRequirementsFile( {"sources": ["pyproject.toml"]}, address=Address("", target_name="pyproject.toml"), ), expected_targets=[], ) assert "was of type int" in str(exc.value)
def test_non_pep440_error(rule_runner: RuleRunner, caplog: Any) -> None: with pytest.raises(ExecutionError) as exc: assert_poetry_requirements( rule_runner, "poetry_requirements()", """ [tool.poetry.dependencies] foo = "~r62b" [tool.poetry.dev-dependencies] """, expected_file_dep=PythonRequirementsFile( {"sources": ["pyproject.toml"]}, address=Address("", target_name="pyproject.toml"), ), expected_targets=[], ) assert 'Failed to parse requirement foo = "~r62b" in pyproject.toml' in str(exc.value)
def test_properly_creates_extras_requirements(rule_runner: RuleRunner) -> None: """This tests the proper parsing of requirements installed with specified extras.""" assert_pipenv_requirements( rule_runner, "pipenv_requirements()", { "default": { "ansicolors": { "version": ">=1.18.0", "extras": ["neon"] } }, "develop": { "cachetools": { "markers": "python_version ~= '3.5'", "version": "==4.1.1", "extras": ["ring", "mongo"], } }, }, expected_file_dep=PythonRequirementsFile( {"sources": ["Pipfile.lock"]}, Address("", target_name="Pipfile.lock")), expected_targets=[ PythonRequirementLibrary( { "requirements": [Requirement.parse("ansicolors[neon]>=1.18.0")], "dependencies": [":Pipfile.lock"], }, Address("", target_name="ansicolors"), ), PythonRequirementLibrary( { "requirements": [ Requirement.parse( "cachetools[ring,mongo]==4.1.1;python_version ~= '3.5'" ) ], "dependencies": [":Pipfile.lock"], }, Address("", target_name="cachetools"), ), ], )
def test_relpath_override(self) -> None: self.assert_python_requirements( "python_requirements(requirements_relpath='subdir/requirements.txt')", "ansicolors>=1.18.0", requirements_txt_relpath="subdir/requirements.txt", expected_file_dep=PythonRequirementsFile( {"sources": ["subdir/requirements.txt"]}, address=Address("", target_name="subdir/requirements.txt"), ), expected_targets=[ PythonRequirementLibrary( { "dependencies": [":subdir/requirements.txt"], "requirements": [Requirement.parse("ansicolors>=1.18.0")], }, address=Address("", target_name="ansicolors"), ), ], )
def test_source_override(rule_runner: RuleRunner) -> None: assert_python_requirements( rule_runner, "python_requirements(source='subdir/requirements.txt')", "ansicolors>=1.18.0", requirements_txt_relpath="subdir/requirements.txt", expected_file_dep=PythonRequirementsFile( {"sources": ["subdir/requirements.txt"]}, Address("", target_name="subdir_requirements.txt"), ), expected_targets=[ PythonRequirementTarget( { "dependencies": [":subdir_requirements.txt"], "requirements": [PipRequirement.parse("ansicolors>=1.18.0")], }, Address("", target_name="ansicolors"), ), ], )
def test_pyproject_toml(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement for each entry in a pyproject.toml file. Note that this just ensures proper targets are created; see prior tests for specific parsing edge cases. """ assert_poetry_requirements( rule_runner, dedent("""\ poetry_requirements( # module_mapping should work regardless of capitalization. module_mapping={'ansiCOLORS': ['colors']}, type_stubs_module_mapping={'Django-types': ['django']}, ) """), dedent("""\ [tool.poetry.dependencies] Django = {version = "3.2", python = "3"} Django-types = "2" Un-Normalized-PROJECT = "1.0.0" [tool.poetry.dev-dependencies] ansicolors = ">=1.18.0" """), expected_file_dep=PythonRequirementsFile( {"sources": ["pyproject.toml"]}, address=Address("", target_name="pyproject.toml"), ), expected_targets=[ PythonRequirementTarget( { "dependencies": [":pyproject.toml"], "requirements": [PipRequirement.parse("ansicolors>=1.18.0")], "modules": ["colors"], }, address=Address("", target_name="ansicolors"), ), PythonRequirementTarget( { "dependencies": [":pyproject.toml"], "requirements": [ PipRequirement.parse( "Django==3.2 ; python_version == '3'") ], }, address=Address("", target_name="Django"), ), PythonRequirementTarget( { "dependencies": [":pyproject.toml"], "requirements": [PipRequirement.parse("Django-types==2")], "type_stub_modules": ["django"], }, address=Address("", target_name="Django-types"), ), PythonRequirementTarget( { "dependencies": [":pyproject.toml"], "requirements": [PipRequirement.parse("Un_Normalized_PROJECT == 1.0.0")], }, address=Address("", target_name="Un-Normalized-PROJECT"), ), ], )
def test_requirements_txt(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement for each entry in a requirements.txt file, where each dependency is unique. Some edge cases: * We ignore comments and options (values that start with `--`). * If a module_mapping is given, and the project is in the map, we copy over a subset of the mapping to the created target. It works regardless of capitalization. * Projects get normalized thanks to Requirement.parse(). """ assert_python_requirements( rule_runner, dedent("""\ python_requirements( module_mapping={'ansiCOLORS': ['colors']}, type_stubs_module_mapping={'Django-types': ['django']}, ) """), dedent("""\ # Comment. --find-links=https://duckduckgo.com ansicolors>=1.18.0 Django==3.2 ; python_version>'3' Django-types Un-Normalized-PROJECT # Inline comment. pip@ git+https://github.com/pypa/pip.git """), expected_file_dep=PythonRequirementsFile( {"sources": ["requirements.txt"]}, Address("", target_name="requirements.txt"), ), expected_targets=[ PythonRequirementTarget( { "dependencies": [":requirements.txt"], "requirements": [PipRequirement.parse("ansicolors>=1.18.0")], "modules": ["colors"], }, Address("", target_name="ansicolors"), ), PythonRequirementTarget( { "dependencies": [":requirements.txt"], "requirements": [PipRequirement.parse("Django==3.2 ; python_version>'3'")], }, Address("", target_name="Django"), ), PythonRequirementTarget( { "dependencies": [":requirements.txt"], "requirements": [PipRequirement.parse("Django-types")], "type_stub_modules": ["django"], }, Address("", target_name="Django-types"), ), PythonRequirementTarget( { "dependencies": [":requirements.txt"], "requirements": [PipRequirement.parse("Un_Normalized_PROJECT")], }, Address("", target_name="Un-Normalized-PROJECT"), ), PythonRequirementTarget( { "dependencies": [":requirements.txt"], "requirements": [ PipRequirement.parse( "pip@ git+https://github.com/pypa/pip.git") ], }, Address("", target_name="pip"), ), ], )