/
niceSOFT
/
python3-hatchling
Обзор
Документация
Войти
/
niceSOFT
/
python3-hatchling
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/env/plugin/test_interface.py
4 295 строк
141 KB
Cary Hawkins
Add sources to enable other types of local dependencies (#2313)
10 авг 2026, 06:24
Не верифицирован
10 авг 2026, 06:24
a885803
Код
Авторство
О чём код?
import re import pytest from hatch.config.constants import AppEnvVars from hatch.env.plugin.interface import EnvironmentInterface from hatch.project.core import Project from hatch.utils.structures import EnvVars class MockEnvironment(EnvironmentInterface): # no cov PLUGIN_NAME = "mock" def find(self): pass def create(self): pass def remove(self): pass def exists(self): pass def install_project(self): pass def install_project_dev_mode(self): pass def dependencies_in_sync(self): pass def sync_dependencies(self): pass class TestEnvVars: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_vars == environment.env_vars == {AppEnvVars.ENV_ACTIVE: "default"} def test_not_table(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-vars": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.env-vars` must be a mapping"): _ = environment.env_vars def test_value_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-vars": {"foo": 9000}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Environment variable `foo` of field `tool.hatch.envs.default.env-vars` must be a string" ): _ = environment.env_vars def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-vars": {"foo": "bar"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_vars == {AppEnvVars.ENV_ACTIVE: "default", "foo": "bar"} def test_context_formatting(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-vars": {"foo": "{env:FOOBAZ}-{matrix:bar}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {"bar": "42"}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with EnvVars({"FOOBAZ": "baz"}): assert environment.env_vars == {AppEnvVars.ENV_ACTIVE: "default", "foo": "baz-42"} class TestEnvInclude: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_include == environment.env_include == () def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-include": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.env-include` must be an array"): _ = environment.env_include def test_pattern_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-include": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Pattern #1 of field `tool.hatch.envs.default.env-include` must be a string" ): _ = environment.env_include def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-include": ["FOO*"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_include == ("HATCH_BUILD_*", "FOO*") class TestEnvExclude: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_exclude == environment.env_exclude == () def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-exclude": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.env-exclude` must be an array"): _ = environment.env_exclude def test_pattern_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-exclude": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Pattern #1 of field `tool.hatch.envs.default.env-exclude` must be a string" ): _ = environment.env_exclude def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"env-exclude": ["FOO*"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.env_exclude == ("FOO*",) class TestPlatforms: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.platforms == environment.platforms == [] def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"platforms": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.platforms` must be an array"): _ = environment.platforms def test_entry_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"platforms": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Platform #1 of field `tool.hatch.envs.default.platforms` must be a string" ): _ = environment.platforms def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"platforms": ["macOS"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.platforms == ["macos"] class TestSkipInstall: def test_default_project(self, temp_dir, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) (temp_dir / "pyproject.toml").touch() with temp_dir.as_cwd(): assert environment.skip_install is environment.skip_install is False def test_default_no_project(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.skip_install is environment.skip_install is True def test_not_boolean(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"skip-install": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.skip-install` must be a boolean"): _ = environment.skip_install def test_enable(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"skip-install": True}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.skip_install is True class TestDevMode: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dev_mode is environment.dev_mode is True def test_not_boolean(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dev-mode": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.dev-mode` must be a boolean"): _ = environment.dev_mode def test_disable(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dev-mode": False}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dev_mode is False class TestBuilder: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.builder is False def test_not_boolean(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"builder": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.builder` must be a boolean"): _ = environment.builder def test_enable(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"builder": True}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.builder is True class TestFeatures: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.features == environment.features == () def test_invalid_type(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"features": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.features` must be an array of strings"): _ = environment.features def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo-bar": [], "baz": []}}, "tool": {"hatch": {"envs": {"default": {"features": ["Foo...Bar", "Baz", "baZ"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.features == ("baz", "foo-bar") def test_feature_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo": [], "bar": []}}, "tool": {"hatch": {"envs": {"default": {"features": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Feature #1 of field `tool.hatch.envs.default.features` must be a string"): _ = environment.features def test_feature_empty_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo": [], "bar": []}}, "tool": {"hatch": {"envs": {"default": {"features": [""]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match="Feature #1 of field `tool.hatch.envs.default.features` cannot be an empty string" ): _ = environment.features def test_feature_undefined(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "optional-dependencies": {"foo": []}}, "tool": {"hatch": {"envs": {"default": {"features": ["foo", "bar", ""]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match=( "Feature `bar` of field `tool.hatch.envs.default.features` is not defined in " "field `project.optional-dependencies`" ), ): _ = environment.features class TestDependencyGroups: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependency_groups == () def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dependency-groups": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Field `tool.hatch.envs.default.dependency-groups` must be an array of strings" ): _ = environment.dependency_groups def test_correct(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "dependency-groups": {"foo-bar": [], "baz": []}, "tool": {"hatch": {"envs": {"default": {"dependency-groups": ["Foo...Bar", "Baz", "baZ"]}}}}, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependency_groups == ("baz", "foo-bar") def test_group_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "dependency-groups": {"foo": [], "bar": []}, "tool": {"hatch": {"envs": {"default": {"dependency-groups": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Group #1 of field `tool.hatch.envs.default.dependency-groups` must be a string" ): _ = environment.dependency_groups def test_group_empty_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "dependency-groups": {"foo": [], "bar": []}, "tool": {"hatch": {"envs": {"default": {"dependency-groups": [""]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match="Group #1 of field `tool.hatch.envs.default.dependency-groups` cannot be an empty string" ): _ = environment.dependency_groups def test_group_undefined(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": { "name": "my_app", "version": "0.0.1", }, "dependency-groups": {"foo": []}, "tool": {"hatch": {"envs": {"default": {"dependency-groups": ["foo", "bar", ""]}}}}, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) with pytest.raises( ValueError, match=( "Group `bar` of field `tool.hatch.envs.default.dependency-groups` is not " "defined in field `dependency-groups`" ), ): _ = environment.dependency_groups class TestDescription: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.description == environment.description == "" def test_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"description": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.description` must be a string"): _ = environment.description def test_correct(self, isolation, isolated_data_dir, platform, global_application): description = "foo" config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"description": description}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.description is description class TestDependencies: def test_default(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"skip-install": False}}}}, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependencies == environment.dependencies == ("dep1",) assert len(environment.dependencies) == len(environment.dependencies_complex) def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"dependencies": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.dependencies` must be an array"): _ = environment.dependencies def test_entry_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"dependencies": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Dependency #1 of field `tool.hatch.envs.default.dependencies` must be a string" ): _ = environment.dependencies def test_invalid(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"dependencies": ["foo^1"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match="Dependency #1 of field `tool.hatch.envs.default.dependencies` is invalid: .+" ): _ = environment.dependencies def test_extra_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"extra-dependencies": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.extra-dependencies` must be an array"): _ = environment.dependencies def test_extra_entry_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"extra-dependencies": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Dependency #1 of field `tool.hatch.envs.default.extra-dependencies` must be a string" ): _ = environment.dependencies def test_extra_invalid(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"extra-dependencies": ["foo^1"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match="Dependency #1 of field `tool.hatch.envs.default.extra-dependencies` is invalid: .+" ): _ = environment.dependencies def test_full(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "envs": { "default": {"skip-install": False, "dependencies": ["dep2"], "extra-dependencies": ["dep3"]} } } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependencies == ("dep2", "dep3", "dep1") def test_context_formatting(self, isolation, isolated_data_dir, platform, temp_application, uri_slash_prefix): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "envs": { "default": { "skip-install": False, "dependencies": ["dep2"], "extra-dependencies": ["proj @ {root:uri}"], } } } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) normalized_path = str(isolation).replace("\\", "/") assert environment.dependencies == ("dep2", f"proj @ file:{uri_slash_prefix}{normalized_path}", "dep1") def test_project_dependencies_context_formatting( self, temp_dir, isolated_data_dir, platform, temp_application, uri_slash_prefix ): """ Regression test for context formatting in project dependencies. Ensures that dependencies in [project] section with context variables like {root:parent:uri} are properly formatted before creating Dependency objects. """ # Create a sibling project sibling_project = temp_dir.parent / "sibling-project" sibling_project.mkdir(exist_ok=True) (sibling_project / "pyproject.toml").write_text( """\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "sibling-project" version = "0.0.1" """ ) config = { "project": { "name": "my_app", "version": "0.0.1", "dependencies": ["sibling-project @ {root:parent:uri}/sibling-project"], }, "tool": {"hatch": {"envs": {"default": {"skip-install": False}}}}, } project = Project(temp_dir, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) normalized_parent_path = str(temp_dir.parent).replace("\\", "/") expected_dep = f"sibling-project @ file:{uri_slash_prefix}{normalized_parent_path}/sibling-project" # Verify the dependency was formatted correctly assert expected_dep in environment.dependencies # Verify we can access the path property without errors for dep in environment.project_dependencies_complex: if dep.name == "sibling-project": assert dep.path is not None assert "sibling-project" in str(dep.path) def test_full_skip_install(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "envs": { "default": {"dependencies": ["dep2"], "extra-dependencies": ["dep3"], "skip-install": True} } } }, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("dep2", "dep3") def test_full_skip_install_and_features(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": { "name": "my_app", "version": "0.0.1", "dependencies": ["dep1"], "optional-dependencies": {"feat": ["dep4"]}, }, "tool": { "hatch": { "envs": { "default": { "dependencies": ["dep2"], "extra-dependencies": ["dep3"], "skip-install": True, "features": ["feat"], } } } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependencies == ("dep2", "dep3", "dep4") def test_full_skip_install_and_dependency_groups(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": { "name": "my_app", "version": "0.0.1", "dependencies": ["dep1"], }, "dependency-groups": { "foo": ["dep5"], "bar": ["dep4", {"include-group": "foo"}], }, "tool": { "hatch": { "envs": { "default": { "dependencies": ["dep2"], "extra-dependencies": ["dep3"], "skip-install": True, "dependency-groups": ["bar"], } } } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependencies == ("dep2", "dep3", "dep4", "dep5") def test_full_no_dev_mode(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "envs": {"default": {"dependencies": ["dep2"], "extra-dependencies": ["dep3"], "dev-mode": False}} } }, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("dep2", "dep3") def test_builder(self, isolation, isolated_data_dir, platform, global_application): config = { "build-system": {"requires": ["dep2"]}, "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": {"envs": {"default": {"skip-install": False, "builder": True, "dependencies": ["dep3"]}}} }, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("dep3", "dep2") def test_workspace(self, temp_dir, isolated_data_dir, platform, temp_application): for i in range(3): project_file = temp_dir / f"foo{i}" / "pyproject.toml" project_file.parent.mkdir() project_file.write_text( f"""\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "foo{i}" version = "0.0.1" dependencies = ["pkg-{i}"] [project.optional-dependencies] feature1 = ["pkg-feature-1{i}"] feature2 = ["pkg-feature-2{i}"] feature3 = ["pkg-feature-3{i}"] """ ) config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "envs": { "default": { "skip-install": False, "dependencies": ["dep2"], "extra-dependencies": ["dep3"], "workspace": { "members": [ {"path": "foo0", "features": ["feature1"]}, {"path": "foo1", "features": ["feature1", "feature2"]}, {"path": "foo2", "features": ["feature1", "feature2", "feature3"]}, ], }, }, }, }, }, } project = Project(temp_dir, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert environment.dependencies == ( "dep2", "dep3", "pkg-0", "pkg-feature-10", "pkg-1", "pkg-feature-11", "pkg-feature-21", "pkg-2", "pkg-feature-12", "pkg-feature-22", "pkg-feature-32", "dep1", ) def test_self_referencing_dependency_with_extras(self, temp_dir, isolated_data_dir, platform, global_application): """Test that self-referencing dependencies with extras include the extra's dependencies.""" project_dir = temp_dir / "my-app" project_dir.mkdir() (project_dir / "my_app").mkdir() (project_dir / "my_app" / "__init__.py").write_text("") (project_dir / "my_app" / "__about__.py").write_text('__version__ = "0.0.1"') config = { "project": { "name": "my-app", "version": "0.0.1", "dependencies": [], "optional-dependencies": { "test": ["pytest>=7.0"], }, }, "tool": { "hatch": { "envs": { "dev": { "skip-install": False, "dependencies": ["my-app[test]"], } } } }, } project = Project(project_dir, config=config) global_application.project = project environment = MockEnvironment( project_dir, project.metadata, "dev", project.config.envs["dev"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) # Get all dependencies as strings (what would be passed to pip) all_deps_str = [str(d) for d in environment.all_dependencies_complex] # Should have the local installation assert any("my-app" in dep and "file://" in dep for dep in all_deps_str) # Should have my-app[test] which will cause pip to install pytest assert any("pytest" in dep.lower() for dep in all_deps_str) def test_workspace_member_dependency_with_extras(self, temp_dir, isolated_data_dir, platform, temp_application): """A workspace member's extras must expand against that member's own optional-dependencies, not the root project's. """ member_file = temp_dir / "member-pkg" / "pyproject.toml" member_file.parent.mkdir() member_file.write_text( """\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "member-pkg" version = "0.0.1" dependencies = [] [project.optional-dependencies] test = ["member-test-dep"] """ ) config = { "project": { "name": "my-app", "version": "0.0.1", "dependencies": [], "optional-dependencies": { # Same extra name on the root project, with a *different* dependency, # to prove the lookup uses the member's metadata not the root's. "test": ["root-test-dep"], }, }, "tool": { "hatch": { "envs": { "default": { "skip-install": False, "dependencies": ["member-pkg[test]"], "workspace": {"members": [{"path": "member-pkg"}]}, }, }, }, }, } project = Project(temp_dir, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) all_deps_str = [str(d) for d in environment.all_dependencies_complex] # The member's own extra must be expanded assert any("member-test-dep" in dep for dep in all_deps_str) # The root project's same-named extra must NOT leak into the env assert not any("root-test-dep" in dep for dep in all_deps_str) def test_self_referencing_dependency_with_extras_and_metadata_hook( self, temp_dir, isolated_data_dir, platform, global_application, helpers ): """Regression test for https://github.com/pypa/hatch/issues/2252. A configured metadata hook must not cause extras of self-referencing dependencies to be dropped. The hook here only sets ``description``; ``optional-dependencies`` remain static and the extras of ``my-app[test]`` must still expand to ``pytest>=7.0``. """ project_dir = temp_dir / "my-app" project_dir.mkdir() (project_dir / "my_app").mkdir() (project_dir / "my_app" / "__init__.py").write_text("") (project_dir / "my_app" / "__about__.py").write_text('__version__ = "0.0.1"') (project_dir / "hatch_build.py").write_text( helpers.dedent( """ from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomHook(MetadataHookInterface): def update(self, metadata): metadata['description'] = 'set by hook' """ ) ) config = { "project": { "name": "my-app", "version": "0.0.1", "dynamic": ["description"], "dependencies": [], "optional-dependencies": { "test": ["pytest>=7.0"], }, }, "tool": { "hatch": { "metadata": {"hooks": {"custom": {}}}, "envs": { "dev": { "skip-install": False, "dependencies": ["my-app[test]"], } }, } }, } project = Project(project_dir, config=config) global_application.project = project environment = MockEnvironment( project_dir, project.metadata, "dev", project.config.envs["dev"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) all_deps_str = [str(d) for d in environment.all_dependencies_complex] # The self-referenced project must still be installed locally assert any("my-app" in dep and "file://" in dep for dep in all_deps_str) # And the extras must be expanded even though a metadata hook is configured assert any("pytest" in dep.lower() for dep in all_deps_str) def test_self_referencing_dependency_with_extras_and_unresolvable_metadata_hook( self, temp_dir, isolated_data_dir, platform, global_application ): """Regression test for https://github.com/pypa/hatch/issues/2345. A metadata hook that can't be resolved by hatch's own plugin manager (e.g. a third-party hook plugin that is only installed in the project's build environment, not in hatch's own venv) must not crash extra expansion for a self-referencing dependency, as long as `optional-dependencies` itself is static. """ project_dir = temp_dir / "my-app" project_dir.mkdir() config = { "project": { "name": "my-app", "version": "0.0.1", "dynamic": ["description"], "dependencies": [], "optional-dependencies": { "test": ["pytest>=7.0"], }, }, "tool": { "hatch": { # This hook is not registered anywhere (no `hatch_build.py`, no installed # plugin), simulating a third-party metadata hook that is only available in # the project's own build environment. "metadata": {"hooks": {"docstring-description": {}}}, "envs": { "dev": { "skip-install": False, "dependencies": ["my-app[test]"], } }, } }, } project = Project(project_dir, config=config) global_application.project = project environment = MockEnvironment( project_dir, project.metadata, "dev", project.config.envs["dev"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) all_deps_str = [str(d) for d in environment.all_dependencies_complex] # The self-referenced project must still be installed locally assert any("my-app" in dep and "file://" in dep for dep in all_deps_str) # And the extras must be expanded without hatch needing to resolve the unrelated, # unresolvable metadata hook in its own process assert any("pytest" in dep.lower() for dep in all_deps_str) def test_dev_mode_true_returns_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dev-mode=true creates editable local dependency.""" # Create a pyproject.toml file so skip_install defaults to False pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] dev-mode = true """) project = Project(temp_dir) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 1 assert local_deps[0].editable is True def test_dev_mode_false_returns_non_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dev-mode=false creates non-editable local dependency.""" # Create a pyproject.toml file so skip_install defaults to False pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] dev-mode = false """) project = Project(temp_dir) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 1 assert local_deps[0].editable is False def test_skip_install_returns_empty(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify skip-install=true returns empty local dependencies.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] skip-install = true """) project = Project(temp_dir) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 0 def test_workspace_members_always_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify workspace members are always editable regardless of dev-mode.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] dev-mode = false """) project = Project(temp_dir) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex # Project itself should respect dev-mode=false assert len(local_deps) == 1 assert local_deps[0].editable is False def test_dependency_group_resolution(self, temp_dir, isolated_data_dir, platform, temp_application): """Test dependency group resolution.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [dependency-groups] test = ["pytest"] [tool.hatch.envs.default] dependency-groups = ["test"] """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.project_dependencies_complex assert any("pytest" in str(d) for d in deps) def test_dependency_group_resolution_builder_false_dev_mode_false( self, temp_dir, isolated_data_dir, platform, temp_application ): """Test dependency group resolution in non-builder non-dev-mode environments.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [dependency-groups] test = ["pytest"] [tool.hatch.envs.default] builder = false dev-mode = false dependency-groups = ["test"] """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert any("pytest" in str(d) for d in environment.project_dependencies_complex) assert any("pytest" in str(d) for d in environment.dependencies_complex) def test_dependency_group_resolution_builder_true_dev_mode_false( self, temp_dir, isolated_data_dir, platform, temp_application ): """Test dependency group resolution in builder non-dev-mode environments.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [dependency-groups] test = ["pytest"] [tool.hatch.envs.default] builder = true dev-mode = false dependency-groups = ["test"] """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert any("pytest" in str(d) for d in environment.project_dependencies_complex) assert any("pytest" in str(d) for d in environment.dependencies_complex) def test_dependency_group_resolution_builder_true_dev_mode_true( self, temp_dir, isolated_data_dir, platform, temp_application ): """Test dependency group resolution in builder dev-mode environments.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [dependency-groups] test = ["pytest"] [tool.hatch.envs.default] builder = true dev-mode = true dependency-groups = ["test"] """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert any("pytest" in str(d) for d in environment.project_dependencies_complex) assert any("pytest" in str(d) for d in environment.dependencies_complex) def test_additional_dependencies_as_strings(self, temp_dir, isolated_data_dir, platform, temp_application): """Test additional_dependencies with string values.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) environment.additional_dependencies = ["extra-dep"] deps = environment.dependencies_complex assert any("extra-dep" in str(d) for d in deps) class TestScripts: @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_not_table(self, isolation, isolated_data_dir, platform, global_application, field): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match=f"Field `tool.hatch.envs.default.{field}` must be a table"): _ = environment.scripts @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_name_contains_spaces(self, isolation, isolated_data_dir, platform, global_application, field): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: {"foo bar": []}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match=f"Script name `foo bar` in field `tool.hatch.envs.default.{field}` must not contain spaces", ): _ = environment.scripts def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == environment.scripts == {} @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_single_commands(self, isolation, isolated_data_dir, platform, global_application, field): script_config = {"foo": "command1", "bar": "command2"} config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == {"foo": ["command1"], "bar": ["command2"]} @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_multiple_commands(self, isolation, isolated_data_dir, platform, global_application, field): script_config = {"foo": "command1", "bar": ["command3", "command2"]} config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == {"foo": ["command1"], "bar": ["command3", "command2"]} @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_multiple_commands_not_string(self, isolation, isolated_data_dir, platform, global_application, field): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: {"foo": [9000]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match=f"Command #1 in field `tool.hatch.envs.default.{field}.foo` must be a string" ): _ = environment.scripts @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_config_invalid_type(self, isolation, isolated_data_dir, platform, global_application, field): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: {"foo": 9000}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match=f"Field `tool.hatch.envs.default.{field}.foo` must be a string or an array of strings" ): _ = environment.scripts @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_command_expansion_basic(self, isolation, isolated_data_dir, platform, global_application, field): script_config = {"foo": "command1", "bar": ["command3", "foo"]} config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == {"foo": ["command1"], "bar": ["command3", "command1"]} @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_command_expansion_multiple_nested(self, isolation, isolated_data_dir, platform, global_application, field): script_config = { "foo": "command3", "baz": ["command5", "bar", "foo", "command1"], "bar": ["command4", "foo", "command2"], } config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == { "foo": ["command3"], "baz": ["command5", "command4", "command3", "command2", "command3", "command1"], "bar": ["command4", "command3", "command2"], } @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_command_expansion_multiple_nested_ignore_exit_code( self, isolation, isolated_data_dir, platform, global_application, field ): script_config = { "foo": "command3", "baz": ["command5", "- bar", "foo", "command1"], "bar": ["command4", "- foo", "command2"], } config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == { "foo": ["command3"], "baz": ["command5", "- command4", "- command3", "- command2", "command3", "command1"], "bar": ["command4", "- command3", "command2"], } @pytest.mark.parametrize("field", ["scripts", "extra-scripts"]) def test_command_expansion_modification(self, isolation, isolated_data_dir, platform, global_application, field): script_config = { "foo": "command3", "baz": ["command5", "bar world", "foo", "command1"], "bar": ["command4", "foo hello", "command2"], } config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {field: script_config}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == { "foo": ["command3"], "baz": ["command5", "command4 world", "command3 hello world", "command2 world", "command3", "command1"], "bar": ["command4", "command3 hello", "command2"], } def test_command_expansion_circular_inheritance(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "bar", "bar": "foo"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match="Circular expansion detected for field `tool.hatch.envs.default.scripts`: foo -> bar -> foo", ): _ = environment.scripts def test_extra_less_precedence(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "envs": { "default": { "extra-scripts": {"foo": "command4", "baz": "command3"}, "scripts": {"foo": "command1", "bar": "command2"}, } } }, }, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.scripts == {"foo": ["command1"], "bar": ["command2"], "baz": ["command3"]} class TestPreInstallCommands: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.pre_install_commands == environment.pre_install_commands == () def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"pre-install-commands": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.pre-install-commands` must be an array"): _ = environment.pre_install_commands def test_entry_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"pre-install-commands": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Command #1 of field `tool.hatch.envs.default.pre-install-commands` must be a string" ): _ = environment.pre_install_commands def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"pre-install-commands": ["baz test"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.pre_install_commands == ("baz test",) class TestPostInstallCommands: def test_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.post_install_commands == environment.post_install_commands == () def test_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"post-install-commands": 9000}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.post-install-commands` must be an array"): _ = environment.post_install_commands def test_entry_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"post-install-commands": [9000]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match="Command #1 of field `tool.hatch.envs.default.post-install-commands` must be a string" ): _ = environment.post_install_commands def test_correct(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"post-install-commands": ["baz test"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.post_install_commands == ("baz test",) class TestEnvVarOption: def test_unset(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.get_env_var_option("foo") == "" def test_set(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with EnvVars({"HATCH_ENV_TYPE_MOCK_FOO": "bar"}): assert environment.get_env_var_option("foo") == "bar" class TestContextFormatting: def test_env_name(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {env_name}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo")) == ["command default"] def test_env_type(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {env_type}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo")) == ["command mock"] def test_verbosity_default(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command -v={verbosity}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 9000, global_application, ) assert list(environment.expand_command("foo")) == ["command -v=9000"] def test_verbosity_unknown_modifier(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {verbosity:bar}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(ValueError, match="Unknown verbosity modifier: bar"): next(environment.expand_command("foo")) def test_verbosity_flag_adjustment_not_integer(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {verbosity:flag:-1.0}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Verbosity flag adjustment must be an integer: -1.0"): next(environment.expand_command("foo")) @pytest.mark.parametrize( ("verbosity", "command"), [ (-9000, "command -qqq"), (-3, "command -qqq"), (-2, "command -qq"), (-1, "command -q"), (0, "command"), (1, "command -v"), (2, "command -vv"), (3, "command -vvv"), (9000, "command -vvv"), ], ) def test_verbosity_flag_default( self, isolation, isolated_data_dir, platform, global_application, verbosity, command ): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {verbosity:flag}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, verbosity, global_application, ) assert list(environment.expand_command("foo")) == [command] @pytest.mark.parametrize( ("adjustment", "command"), [ (-9000, "command -qqq"), (-3, "command -qqq"), (-2, "command -qq"), (-1, "command -q"), (0, "command"), (1, "command -v"), (2, "command -vv"), (3, "command -vvv"), (9000, "command -vvv"), ], ) def test_verbosity_flag_adjustment( self, isolation, isolated_data_dir, platform, global_application, adjustment, command ): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": f"command {{verbosity:flag:{adjustment}}}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo")) == [command] def test_verbosity_flag_adjustment_invalid(self, temp_dir, isolated_data_dir, platform, temp_application): """Test verbosity flag with invalid adjustment.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] scripts.test = "pytest {verbosity:flag:invalid}" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) with pytest.raises(TypeError, match="Verbosity flag adjustment must be an integer"): list(environment.expand_command("test")) def test_args_undefined(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {args}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo")) == ["command"] def test_args_default(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {args: -bar > /dev/null}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo")) == ["command -bar > /dev/null"] def test_args_default_override(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"scripts": {"foo": "command {args: -bar > /dev/null}"}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert list(environment.expand_command("foo baz")) == ["command baz"] def test_matrix_no_selection(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dependencies": ["pkg=={matrix}"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(ValueError, match="The `matrix` context formatting field requires a modifier"): _ = environment.dependencies def test_matrix_no_default(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dependencies": ["pkg=={matrix:bar}"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(ValueError, match="Nonexistent matrix variable must set a default: bar"): _ = environment.dependencies def test_matrix_default(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dependencies": ["pkg=={matrix:bar:9000}"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("pkg==9000",) def test_matrix_default_override(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"dependencies": ["pkg=={matrix:bar:baz}"]}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {"bar": "42"}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("pkg==42",) def test_env_vars_override(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "envs": { "default": { "dependencies": ["pkg{env:DEP_PIN}"], "env-vars": {"DEP_PIN": "==0.0.1"}, "overrides": {"env": {"DEP_ANY": {"env-vars": "DEP_PIN="}}}, }, }, }, }, } with EnvVars({"DEP_ANY": "true"}): project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.dependencies == ("pkg",) class TestWorkspaceConfig: def test_not_table(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": 9000}}}}, } project = Project(isolation, config=config) with pytest.raises(TypeError, match="Field workspace must be a table"): MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], # Exception raised here {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) def test_parallel_not_boolean(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"parallel": 9000}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises(TypeError, match="Field `tool.hatch.envs.default.workspace.parallel` must be a boolean"): _ = environment.workspace.parallel def test_parallel_default(self, isolation, isolated_data_dir, platform, global_application): config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.workspace.parallel is True def test_parallel_override(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"parallel": False}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.workspace.parallel is False def test_members_not_table(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": 9000}}}}}, } project = Project(isolation, config=config) with pytest.raises(TypeError, match="Field workspace.members must be an array"): MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], # Exception raised here {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) def test_member_invalid_type(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [9000]}}}}}, } project = Project(isolation, config=config) with pytest.raises(TypeError, match="Member #1 must be a string or table"): MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], # Exception raised here {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) def test_member_no_path(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{}]}}}}}, } project = Project(isolation, config=config) with pytest.raises(TypeError, match="Member #1 must define a `path` key"): MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], # Exception raised here {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) def test_member_path_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": 9000}]}}}}}, } project = Project(isolation, config=config) with pytest.raises(TypeError, match="Member #1 path must be a string"): MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], # Exception raised here {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) def test_member_path_empty_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": ""}]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match=( "Option `path` of member #1 of field `tool.hatch.envs.default.workspace.members` " "cannot be an empty string" ), ): _ = environment.workspace.members def test_member_features_not_array(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo", "features": 9000}]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match=( "Option `features` of member #1 of field `tool.hatch.envs.default.workspace.members` " "must be an array of strings" ), ): _ = environment.workspace.members def test_member_feature_not_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo", "features": [9000]}]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( TypeError, match=( "Feature #1 of option `features` of member #1 of field `tool.hatch.envs.default.workspace.members` " "must be a string" ), ): _ = environment.workspace.members def test_member_feature_empty_string(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo", "features": [""]}]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match=( "Feature #1 of option `features` of member #1 of field `tool.hatch.envs.default.workspace.members` " "cannot be an empty string" ), ): _ = environment.workspace.members def test_member_feature_duplicate(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "envs": {"default": {"workspace": {"members": [{"path": "foo", "features": ["foo", "Foo"]}]}}} } }, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( ValueError, match=( "Feature #2 of option `features` of member #1 of field `tool.hatch.envs.default.workspace.members` " "is a duplicate" ), ): _ = environment.workspace.members def test_member_does_not_exist(self, isolation, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo"}]}}}}}, } project = Project(isolation, config=config) environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) with pytest.raises( OSError, match=re.escape( f"No members could be derived from `foo` of field `tool.hatch.envs.default.workspace.members`: " f"{isolation / 'foo'}" ), ): _ = environment.workspace.members def test_member_not_project(self, temp_dir, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo"}]}}}}}, } project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) member_path = temp_dir / "foo" member_path.mkdir() with pytest.raises( OSError, match=re.escape( f"Member derived from `foo` of field `tool.hatch.envs.default.workspace.members` is not a project " f"(no `pyproject.toml` file): {member_path}" ), ): _ = environment.workspace.members def test_member_duplicate(self, temp_dir, isolated_data_dir, platform, global_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo"}, {"path": "f*"}]}}}}}, } project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) member_path = temp_dir / "foo" member_path.mkdir() (member_path / "pyproject.toml").touch() with pytest.raises( ValueError, match=re.escape( f"Member derived from `f*` of field " f"`tool.hatch.envs.default.workspace.members` is a duplicate: {member_path}" ), ): _ = environment.workspace.members def test_correct(self, hatch, temp_dir, isolated_data_dir, platform, global_application): member1_path = temp_dir / "foo" member2_path = temp_dir / "bar" member3_path = temp_dir / "baz" for member_path in [member1_path, member2_path, member3_path]: with temp_dir.as_cwd(): result = hatch("new", member_path.name) assert result.exit_code == 0, result.output config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "foo"}, {"path": "b*"}]}}}}}, } project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) members = environment.workspace.members assert len(members) == 3 assert members[0].project.location == member1_path assert members[1].project.location == member2_path assert members[2].project.location == member3_path def test_member_outside_root_with_shared_prefix(self, temp_dir, isolated_data_dir, platform, global_application): """Verify correct workspace member discovery with shared path prefix. os.path.commonprefix works character-by-character, so for paths that share a partial directory name (e.g. 'local_app' and 'lib_member' both start with 'l'), it would return an invalid path like '.../l' instead of the true common ancestor directory. os.path.commonpath correctly returns the nearest common directory, which is what we need as the base for the member glob search. Example of the mismatch: os.path.commonprefix(['/usr/lib', '/usr/local/lib']) == '/usr/l' os.path.commonpath(['/usr/lib', '/usr/local/lib']) == '/usr' """ project_root = temp_dir / "local_app" project_root.mkdir() member_path = temp_dir / "lib_member" member_path.mkdir() (member_path / "pyproject.toml").write_text( """\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "lib-member" version = "0.1.0" """ ) config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "../lib_member"}]}}}}}, } project = Project(project_root, config=config) environment = MockEnvironment( project_root, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) members = environment.workspace.members assert len(members) == 1 assert members[0].project.location == member_path class TestWorkspaceDependencies: def test_basic(self, temp_dir, isolated_data_dir, platform, global_application): for i in range(3): project_file = temp_dir / f"foo{i}" / "pyproject.toml" project_file.parent.mkdir() project_file.write_text( f"""\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "foo{i}" version = "0.0.1" dependencies = ["pkg-{i}"] """ ) config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"workspace": {"members": [{"path": "f*"}]}}}}}, } project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.workspace.get_dependencies() == ["pkg-0", "pkg-1", "pkg-2"] def test_features(self, temp_dir, isolated_data_dir, platform, global_application): for i in range(3): project_file = temp_dir / f"foo{i}" / "pyproject.toml" project_file.parent.mkdir() project_file.write_text( f"""\ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "foo{i}" version = "0.0.1" dependencies = ["pkg-{i}"] [project.optional-dependencies] feature1 = ["pkg-feature-1{i}"] feature2 = ["pkg-feature-2{i}"] feature3 = ["pkg-feature-3{i}"] """ ) config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "envs": { "default": { "workspace": { "members": [ {"path": "foo0", "features": ["feature1"]}, {"path": "foo1", "features": ["feature1", "feature2"]}, {"path": "foo2", "features": ["feature1", "feature2", "feature3"]}, ], }, }, }, }, }, } project = Project(temp_dir, config=config) environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, global_application, ) assert environment.workspace.get_dependencies() == [ "pkg-0", "pkg-feature-10", "pkg-1", "pkg-feature-11", "pkg-feature-21", "pkg-2", "pkg-feature-12", "pkg-feature-22", "pkg-feature-32", ] class TestDependencyHash: def test_hash_includes_local_dependencies(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dependency hash includes local dependencies.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(temp_dir, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) hash_value = environment.dependency_hash() assert hash_value assert len(hash_value) > 0 def test_hash_stable_when_dependencies_unchanged(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dependency hash is stable when dependencies don't change.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) config = {"project": {"name": "my_app", "version": "0.0.1"}} project = Project(temp_dir, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) hash1 = environment.dependency_hash() hash2 = environment.dependency_hash() assert hash1 == hash2 def test_hash_changes_with_extra_dependencies(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dependency hash changes when extra-dependencies are added.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) config_no_deps = {"project": {"name": "my_app", "version": "0.0.1"}} project_no_deps = Project(temp_dir, config=config_no_deps) project_no_deps.set_app(temp_application) temp_application.project = project_no_deps env_no_deps = MockEnvironment( temp_dir, project_no_deps.metadata, "default", project_no_deps.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) hash_no_deps = env_no_deps.dependency_hash() config_with_deps = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": {"hatch": {"envs": {"default": {"extra-dependencies": ["pytest"]}}}}, } project_with_deps = Project(temp_dir, config=config_with_deps) project_with_deps.set_app(temp_application) temp_application.project = project_with_deps env_with_deps = MockEnvironment( temp_dir, project_with_deps.metadata, "default", project_with_deps.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) hash_with_deps = env_with_deps.dependency_hash() assert hash_no_deps != hash_with_deps class TestLocalDependenciesComplex: def test_dev_mode_true_returns_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dev-mode=true creates editable local dependency.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 1 assert local_deps[0].editable is True def test_dev_mode_false_returns_non_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dev-mode=false creates non-editable local dependency.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] dev-mode = false """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 1 assert local_deps[0].editable is False def test_workspace_members_always_editable(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify workspace members are always editable regardless of dev-mode.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.default] dev-mode = false workspace.members = ["member"] """) member_dir = temp_dir / "member" member_dir.mkdir() (member_dir / "pyproject.toml").write_text(""" [project] name = "member" version = "0.0.1" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) local_deps = environment.local_dependencies_complex assert len(local_deps) == 2 project_dep = next(d for d in local_deps if d.name == "my-app") member_dep = next(d for d in local_deps if d.name == "member") assert project_dep.editable is False assert member_dep.editable is True class TestDynamicDependencies: def test_dynamic_dependencies_resolved(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify dynamic dependencies are resolved correctly.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" dynamic = ["dependencies"] [tool.hatch.metadata] allow-direct-references = true """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert "dependencies" in environment.metadata.dynamic class TestBuildSystemIntegration: def test_builder_includes_build_requirements(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify builder environment includes build system requirements.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [build-system] requires = ["hatchling", "build-dep"] [project] name = "my-app" version = "0.0.1" [tool.hatch.envs.build] builder = true """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "build", project.config.envs["build"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.dependencies assert any("hatchling" in d for d in deps) assert any("build-dep" in d for d in deps) class TestEnvironmentLifecycle: def test_app_status_contexts(self, temp_dir, isolated_data_dir, platform, temp_application): """Verify environment lifecycle status contexts work correctly.""" pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) with environment.app_status_creation(): pass with environment.app_status_pre_installation(): pass with environment.app_status_post_installation(): pass with environment.app_status_project_installation(): pass with environment.app_status_dependency_state_check(): pass with environment.app_status_dependency_installation_check(): pass with environment.app_status_dependency_synchronization(): pass class TestFileSystemContext: def test_join_creates_new_context(self, temp_dir, isolated_data_dir, platform, temp_application): """Test FileSystemContext.join creates proper paths.""" from hatch.env.plugin.interface import FileSystemContext pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) ctx = FileSystemContext(environment, local_path=temp_dir, env_path="/env") new_ctx = ctx.join("subdir") assert "subdir" in str(new_ctx.local_path) assert "subdir" in new_ctx.env_path class TestEnvironmentSources: def test_path_source_rewrites_dependency(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "sources": {"dep1": "./packages/dep1"}, "envs": {"default": {"skip-install": False}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.project_dependencies_complex assert len(deps) == 1 assert deps[0].url is not None assert deps[0].url.startswith("file://") assert deps[0].editable is True def test_index_source_becomes_install_flag(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "sources": {"dep1": {"index": "https://pypi.example.com/simple"}}, "envs": {"default": {"skip-install": False}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.project_dependencies_complex assert len(deps) == 1 assert deps[0].url is None global_args = environment.get_source_install_args(deps) assert global_args == ["--extra-index-url", "https://pypi.example.com/simple"] def test_environment_dependencies_decorated(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "sources": {"dep2": {"git": "https://example.com/dep2", "rev": "abc"}}, "envs": {"default": {"dependencies": ["dep2"]}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.environment_dependencies_complex assert len(deps) == 1 assert deps[0].url == "git+https://example.com/dep2@abc" def test_no_sources_is_noop(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": {"hatch": {"envs": {"default": {"skip-install": False}}}}, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.project_dependencies_complex assert len(deps) == 1 assert deps[0].url is None assert environment.get_source_install_args(deps) == [] def test_workspace_source_resolves_to_member(self, temp_dir, isolated_data_dir, platform, temp_application): member_dir = temp_dir / "packages" / "dep1" member_dir.ensure_dir_exists() (member_dir / "pyproject.toml").write_text('[project]\nname = "dep1"\nversion = "0.0.1"\n') pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" dependencies = ["dep1"] [tool.hatch.sources] dep1 = { workspace = true } [tool.hatch.envs.default] skip-install = false workspace.members = ["packages/*"] """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) workspace_deps = [dep for dep in environment.project_dependencies_complex if dep.name == "dep1"] assert len(workspace_deps) == 1 dep = workspace_deps[0] assert dep.url is not None assert dep.url.startswith("file://") assert dep.url.endswith("packages/dep1") assert dep.editable is True def test_workspace_source_without_member_errors(self, temp_dir, isolated_data_dir, platform, temp_application): pyproject = temp_dir / "pyproject.toml" pyproject.write_text(""" [project] name = "my-app" version = "0.0.1" dependencies = ["dep1"] [tool.hatch.sources] dep1 = { workspace = true } [tool.hatch.envs.default] skip-install = false """) project = Project(temp_dir) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( temp_dir, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) with pytest.raises( ValueError, match="Dependency `dep1` declares `workspace = true` in its environment's `sources`", ): _ = environment.project_dependencies_complex def test_env_sources_override_global(self, isolation, isolated_data_dir, platform, temp_application): from hatch.project.sources import GitSource, PathSource config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "sources": {"dep1": "./packages/dep1"}, "envs": { "default": {"skip-install": False}, "upstream": { "skip-install": False, "sources": {"dep1": {"git": "https://example.com/dep1", "branch": "main"}}, }, }, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environments = {} for env_name in ("default", "upstream"): environments[env_name] = MockEnvironment( isolation, project.metadata, env_name, project.config.envs[env_name], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) assert isinstance(environments["default"].sources["dep1"], PathSource) assert isinstance(environments["upstream"].sources["dep1"], GitSource) deps = environments["upstream"].project_dependencies_complex assert len(deps) == 1 assert deps[0].url == "git+https://example.com/dep1@main" def test_env_sources_error_location(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "envs": {"default": {"sources": {"dep1": {"path": 9000}}}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) with pytest.raises( TypeError, match=re.escape("Field `tool.hatch.envs.default.sources.dep1.path` must be a string") ): _ = environment.sources def test_no_sources_env_var(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1", "dependencies": ["dep1"]}, "tool": { "hatch": { "sources": {"dep1": "./packages/dep1"}, "envs": {"default": {"skip-install": False}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project with EnvVars({AppEnvVars.NO_SOURCES: "1"}): environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) deps = environment.project_dependencies_complex assert len(deps) == 1 assert deps[0].url is None def test_additional_dependencies_decorated(self, isolation, isolated_data_dir, platform, temp_application): config = { "project": {"name": "my_app", "version": "0.0.1"}, "tool": { "hatch": { "sources": {"dep3": {"git": "https://example.com/dep3", "rev": "abc"}}, "envs": {"default": {}}, } }, } project = Project(isolation, config=config) project.set_app(temp_application) temp_application.project = project environment = MockEnvironment( isolation, project.metadata, "default", project.config.envs["default"], {}, isolated_data_dir, isolated_data_dir, platform, 0, temp_application, ) environment.additional_dependencies = ["dep3"] deps = [dep for dep in environment.dependencies_complex if dep.name == "dep3"] assert len(deps) == 1 assert deps[0].url == "git+https://example.com/dep3@abc"