/
niceSOFT
/
python3-hatchling
Обзор
Документация
Войти
/
niceSOFT
/
python3-hatchling
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/backend/metadata/test_core.py
1 887 строк
74 KB
Edgar Ramírez Mondragón
fix: Allow the `; private` annotation in `import-names`/`import-namespaces` pyproject entries (#2366)
10 авг 2026, 04:05
Не верифицирован
10 авг 2026, 04:05
bb90277
Код
Авторство
О чём код?
import pytest from hatchling.metadata.core import BuildMetadata, CoreMetadata, HatchMetadata, ProjectMetadata from hatchling.metadata.spec import ( LATEST_METADATA_VERSION, get_core_metadata_constructors, project_metadata_from_core_metadata, ) from hatchling.plugin.manager import PluginManager from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT from hatchling.version.source.regex import RegexSource @pytest.fixture(scope="module") def latest_spec(): return get_core_metadata_constructors()[LATEST_METADATA_VERSION] class TestConfig: def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None) assert metadata.config == metadata.config == {} def test_reuse(self, isolation): config = {} metadata = ProjectMetadata(str(isolation), None, config) assert metadata.config is metadata.config is config def test_read(self, temp_dir): project_file = temp_dir / "pyproject.toml" project_file.write_text("foo = 5") with temp_dir.as_cwd(): metadata = ProjectMetadata(str(temp_dir), None) assert metadata.config == metadata.config == {"foo": 5} class TestInterface: def test_types(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert isinstance(metadata.core, CoreMetadata) assert isinstance(metadata.hatch, HatchMetadata) assert isinstance(metadata.build, BuildMetadata) def test_missing_core_metadata(self, isolation): metadata = ProjectMetadata(str(isolation), None, {}) with pytest.raises(ValueError, match="Missing `project` metadata table in configuration"): _ = metadata.core def test_core_metadata_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": "foo"}) with pytest.raises(TypeError, match="The `project` configuration must be a table"): _ = metadata.core def test_tool_metadata_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"tool": "foo"}) with pytest.raises(TypeError, match="The `tool` configuration must be a table"): _ = metadata.hatch def test_hatch_metadata_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"tool": {"hatch": "foo"}}) with pytest.raises(TypeError, match="The `tool.hatch` configuration must be a table"): _ = metadata.hatch def test_build_metadata_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"build-system": "foo"}) with pytest.raises(TypeError, match="The `build-system` configuration must be a table"): _ = metadata.build class TestDynamic: def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": 10}}) with pytest.raises(TypeError, match="Field `project.dynamic` must be an array"): _ = metadata.core.dynamic def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": [10]}}) with pytest.raises(TypeError, match="Field #1 of field `project.dynamic` must be a string"): _ = metadata.core.dynamic def test_correct(self, isolation): dynamic = ["version"] metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": dynamic}}) assert metadata.core.dynamic == ["version"] def test_cache_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": 10}}) with pytest.raises(TypeError, match="Field `project.dynamic` must be an array"): _ = metadata.dynamic def test_cache_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": [10]}}) with pytest.raises(TypeError, match="Field #1 of field `project.dynamic` must be a string"): _ = metadata.dynamic def test_cache_correct(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": {"name": "foo", "dynamic": ["version", "description"]}, "tool": {"hatch": {"version": {"path": "a/b"}, "metadata": {"hooks": {"custom": {}}}}}, }, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') file_path = temp_dir / DEFAULT_BUILD_SCRIPT file_path.write_text( helpers.dedent( """ from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomHook(MetadataHookInterface): def update(self, metadata): metadata['description'] = metadata['name'] + 'bar' """ ) ) # Trigger hooks with `metadata.core` first assert metadata.core.dynamic == [] assert metadata.dynamic == ["version", "description"] class TestRawName: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"name": 9000, "dynamic": ["name"]}}) with pytest.raises( ValueError, match="Static metadata field `name` cannot be present in field `project.dynamic`" ): _ = metadata.core.raw_name def test_missing(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) with pytest.raises(ValueError, match="Missing required field `project.name`"): _ = metadata.core.raw_name def test_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"name": 9000}}) with pytest.raises(TypeError, match="Field `project.name` must be a string"): _ = metadata.core.raw_name def test_invalid(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"name": "my app"}}) with pytest.raises( ValueError, match=( "Required field `project.name` must only contain ASCII letters/digits, underscores, " "hyphens, and periods, and must begin and end with ASCII letters/digits." ), ): _ = metadata.core.raw_name def test_correct(self, isolation): name = "My.App" metadata = ProjectMetadata(str(isolation), None, {"project": {"name": name}}) assert metadata.core.raw_name is metadata.core.raw_name is name class TestName: @pytest.mark.parametrize("name", ["My--App", "My__App", "My..App"]) def test_normalization(self, isolation, name): metadata = ProjectMetadata(str(isolation), None, {"project": {"name": name}}) assert metadata.core.name == metadata.core.name == "my-app" class TestVersion: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"version": 9000, "dynamic": ["version"]}}) with pytest.raises( ValueError, match="Metadata field `version` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.version def test_static_missing(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) with pytest.raises( ValueError, match="Field `project.version` can only be resolved dynamically if `version` is in field `project.dynamic`", ): _ = metadata.version def test_static_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"version": 9000}}) with pytest.raises(TypeError, match="Field `project.version` must be a string"): _ = metadata.version def test_static_invalid(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"version": "0..0"}}) with pytest.raises( ValueError, match="Invalid version `0..0` from field `project.version`, see https://peps.python.org/pep-0440/", ): _ = metadata.version def test_static_normalization(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"version": "0.1.0.0-rc.1"}}) assert metadata.version == metadata.version == "0.1.0.0rc1" assert metadata.core.version == metadata.core.version == "0.1.0.0-rc.1" def test_dynamic_missing(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dynamic": ["version"]}, "tool": {"hatch": {}}}) with pytest.raises(ValueError, match="Missing `tool.hatch.version` configuration"): _ = metadata.version def test_dynamic_not_table(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": "1.0"}}} ) with pytest.raises(TypeError, match="Field `tool.hatch.version` must be a table"): _ = metadata.version def test_dynamic_source_empty(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": ""}}}} ) with pytest.raises( ValueError, match="The `source` option under the `tool.hatch.version` table must not be empty if defined" ): _ = metadata.version.cached def test_dynamic_source_not_string(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": 42}}}} ) with pytest.raises(TypeError, match="Field `tool.hatch.version.source` must be a string"): _ = metadata.version.cached def test_dynamic_unknown_source(self, isolation): metadata = ProjectMetadata( str(isolation), PluginManager(), {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": "foo"}}}}, ) with pytest.raises(ValueError, match="Unknown version source: foo"): _ = metadata.version.cached def test_dynamic_source_regex(self, temp_dir): metadata = ProjectMetadata( str(temp_dir), PluginManager(), {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": "regex", "path": "a/b"}}}}, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') assert metadata.hatch.version.source is metadata.hatch.version.source assert isinstance(metadata.hatch.version.source, RegexSource) assert metadata.hatch.version.cached == metadata.hatch.version.cached == "0.0.1" def test_dynamic_source_regex_invalid(self, temp_dir): metadata = ProjectMetadata( str(temp_dir), PluginManager(), {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": "regex", "path": "a/b"}}}}, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0..0"') with pytest.raises( ValueError, match="Invalid version `0..0` from source `regex`, see https://peps.python.org/pep-0440/" ): _ = metadata.version def test_dynamic_error(self, isolation): metadata = ProjectMetadata( str(isolation), PluginManager(), {"project": {"dynamic": ["version"]}, "tool": {"hatch": {"version": {"source": "regex"}}}}, ) with pytest.raises( ValueError, match="Error getting the version from source `regex`: option `path` must be specified" ): _ = metadata.version.cached class TestDescription: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"description": 9000, "dynamic": ["description"]}}) with pytest.raises( ValueError, match=( "Metadata field `description` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.description def test_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"description": 9000}}) with pytest.raises(TypeError, match="Field `project.description` must be a string"): _ = metadata.core.description def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.description == metadata.core.description == "" def test_custom(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"description": "foo"}}) assert metadata.core.description == metadata.core.description == "foo" def test_normaliza(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"description": "\nfirst line.\r\nsecond line"}}) assert metadata.core.description == metadata.core.description == " first line. second line" class TestReadme: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": 9000, "dynamic": ["readme"]}}) with pytest.raises( ValueError, match="Metadata field `readme` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.readme @pytest.mark.parametrize("attribute", ["readme", "readme_content_type"]) def test_unknown_type(self, isolation, attribute): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": 9000}}) with pytest.raises(TypeError, match="Field `project.readme` must be a string or a table"): _ = getattr(metadata.core, attribute) def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.readme == metadata.core.readme == "" assert metadata.core.readme_content_type == metadata.core.readme_content_type == "text/markdown" assert metadata.core.readme_path == metadata.core.readme_path == "" def test_string_path_unknown_content_type(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": "foo"}}) with pytest.raises( TypeError, match="Unable to determine the content-type based on the extension of readme file: foo" ): _ = metadata.core.readme def test_string_path_nonexistent(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": "foo/bar.md"}}) with pytest.raises(OSError, match="Readme file does not exist: foo/bar\\.md"): _ = metadata.core.readme @pytest.mark.parametrize( ("extension", "content_type"), [(".md", "text/markdown"), (".rst", "text/x-rst"), (".txt", "text/plain")] ) def test_string_correct(self, extension, content_type, temp_dir): metadata = ProjectMetadata(str(temp_dir), None, {"project": {"readme": f"foo/bar{extension}"}}) file_path = temp_dir / "foo" / f"bar{extension}" file_path.ensure_parent_dir_exists() file_path.write_text("test content") assert metadata.core.readme == metadata.core.readme == "test content" assert metadata.core.readme_content_type == metadata.core.readme_content_type == content_type assert metadata.core.readme_path == metadata.core.readme_path == f"foo/bar{extension}" def test_table_content_type_missing(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": {}}}) with pytest.raises(ValueError, match="Field `content-type` is required in the `project.readme` table"): _ = metadata.core.readme def test_table_content_type_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": {"content-type": 5}}}) with pytest.raises(TypeError, match="Field `content-type` in the `project.readme` table must be a string"): _ = metadata.core.readme def test_table_content_type_not_unknown(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": {"content-type": "foo"}}}) with pytest.raises( ValueError, match=( "Field `content-type` in the `project.readme` table must be one of the following: " "text/markdown, text/x-rst, text/plain" ), ): _ = metadata.core.readme def test_table_multiple_options(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"readme": {"content-type": "text/markdown", "file": "", "text": ""}}} ) with pytest.raises(ValueError, match="Cannot specify both `file` and `text` in the `project.readme` table"): _ = metadata.core.readme def test_table_no_option(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"readme": {"content-type": "text/markdown"}}}) with pytest.raises(ValueError, match="Must specify either `file` or `text` in the `project.readme` table"): _ = metadata.core.readme def test_table_file_not_string(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"readme": {"content-type": "text/markdown", "file": 4}}} ) with pytest.raises(TypeError, match="Field `file` in the `project.readme` table must be a string"): _ = metadata.core.readme def test_table_file_nonexistent(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"readme": {"content-type": "text/markdown", "file": "foo/bar.md"}}} ) with pytest.raises(OSError, match="Readme file does not exist: foo/bar\\.md"): _ = metadata.core.readme def test_table_file_correct(self, temp_dir): metadata = ProjectMetadata( str(temp_dir), None, {"project": {"readme": {"content-type": "text/markdown", "file": "foo/bar.markdown"}}} ) file_path = temp_dir / "foo" / "bar.markdown" file_path.ensure_parent_dir_exists() file_path.write_text("test content") assert metadata.core.readme == metadata.core.readme == "test content" assert metadata.core.readme_content_type == metadata.core.readme_content_type == "text/markdown" assert metadata.core.readme_path == metadata.core.readme_path == "foo/bar.markdown" def test_table_text_not_string(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"readme": {"content-type": "text/markdown", "text": 4}}} ) with pytest.raises(TypeError, match="Field `text` in the `project.readme` table must be a string"): _ = metadata.core.readme def test_table_text_correct(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"readme": {"content-type": "text/markdown", "text": "test content"}}} ) assert metadata.core.readme == metadata.core.readme == "test content" assert metadata.core.readme_content_type == metadata.core.readme_content_type == "text/markdown" assert metadata.core.readme_path == metadata.core.readme_path == "" class TestRequiresPython: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"requires-python": 9000, "dynamic": ["requires-python"]}} ) with pytest.raises( ValueError, match=( "Metadata field `requires-python` cannot be both statically defined and " "listed in field `project.dynamic`" ), ): _ = metadata.core.requires_python @pytest.mark.parametrize("attribute", ["requires_python", "python_constraint"]) def test_not_string(self, isolation, attribute): metadata = ProjectMetadata(str(isolation), None, {"project": {"requires-python": 9000}}) with pytest.raises(TypeError, match="Field `project.requires-python` must be a string"): _ = getattr(metadata.core, attribute) def test_invalid(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"requires-python": "^1"}}) with pytest.raises(ValueError, match="Field `project.requires-python` is invalid: .+"): _ = metadata.core.requires_python def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.requires_python == metadata.core.requires_python == "" for major_version in map(str, range(10)): assert metadata.core.python_constraint.contains(major_version) def test_custom(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"requires-python": ">2"}}) assert metadata.core.requires_python == metadata.core.requires_python == ">2" assert not metadata.core.python_constraint.contains("2") assert metadata.core.python_constraint.contains("3") class TestLicense: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": 9000, "dynamic": ["license"]}}) with pytest.raises( ValueError, match="Metadata field `license` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.license def test_invalid_type(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": 9000}}) with pytest.raises(TypeError, match="Field `project.license` must be a string or a table"): _ = metadata.core.license def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.license == metadata.core.license == "" assert metadata.core.license_expression == metadata.core.license_expression == "" def test_normalization(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": "mit or apache-2.0"}}) assert metadata.core.license_expression == "MIT OR Apache-2.0" def test_invalid_expression(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": "mit or foo"}}) with pytest.raises(ValueError, match="Error parsing field `project.license` - Unknown license: 'foo'"): _ = metadata.core.license_expression def test_multiple_options(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {"file": "", "text": ""}}}) with pytest.raises(ValueError, match="Cannot specify both `file` and `text` in the `project.license` table"): _ = metadata.core.license def test_no_option(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {}}}) with pytest.raises(ValueError, match="Must specify either `file` or `text` in the `project.license` table"): _ = metadata.core.license def test_file_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {"file": 4}}}) with pytest.raises(TypeError, match="Field `file` in the `project.license` table must be a string"): _ = metadata.core.license def test_file_nonexistent(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {"file": "foo/bar.md"}}}) with pytest.raises(OSError, match="License file does not exist: foo/bar\\.md"): _ = metadata.core.license def test_file_correct(self, temp_dir): metadata = ProjectMetadata(str(temp_dir), None, {"project": {"license": {"file": "foo/bar.md"}}}) file_path = temp_dir / "foo" / "bar.md" file_path.ensure_parent_dir_exists() file_path.write_text("test content") assert metadata.core.license == "test content" def test_text_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {"text": 4}}}) with pytest.raises(TypeError, match="Field `text` in the `project.license` table must be a string"): _ = metadata.core.license def test_text_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license": {"text": "test content"}}}) assert metadata.core.license == "test content" class TestLicenseFiles: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"license-files": 9000, "dynamic": ["license-files"]}} ) with pytest.raises( ValueError, match=( "Metadata field `license-files` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.license_files def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license-files": 9000}}) with pytest.raises(TypeError, match="Field `project.license-files` must be an array"): _ = metadata.core.license_files def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"license-files": [9000]}}) with pytest.raises(TypeError, match="Entry #1 of field `project.license-files` must be a string"): _ = metadata.core.license_files def test_default_globs_no_licenses(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.license_files == metadata.core.license_files == [] def test_default_globs_with_licenses(self, temp_dir): metadata = ProjectMetadata(str(temp_dir), None, {"project": {}}) expected = [] (temp_dir / "foo").touch() for name in ("LICENSE", "LICENCE", "COPYING", "NOTICE", "AUTHORS"): (temp_dir / name).touch() expected.append(name) name_with_extension = f"{name}.txt" (temp_dir / f"{name}.txt").touch() expected.append(name_with_extension) assert metadata.core.license_files == sorted(expected) def test_globs_with_licenses(self, temp_dir): metadata = ProjectMetadata(str(temp_dir), None, {"project": {"license-files": ["LICENSES/*"]}}) licenses_dir = temp_dir / "LICENSES" licenses_dir.mkdir() (licenses_dir / "MIT.txt").touch() (licenses_dir / "Apache-2.0.txt").touch() for name in ("LICENSE", "LICENCE", "COPYING", "NOTICE", "AUTHORS"): (temp_dir / name).touch() assert metadata.core.license_files == ["LICENSES/Apache-2.0.txt", "LICENSES/MIT.txt"] def test_paths_with_licenses(self, temp_dir): metadata = ProjectMetadata( str(temp_dir), None, {"project": {"license-files": ["LICENSES/Apache-2.0.txt", "LICENSES/MIT.txt", "COPYING"]}}, ) licenses_dir = temp_dir / "LICENSES" licenses_dir.mkdir() (licenses_dir / "MIT.txt").touch() (licenses_dir / "Apache-2.0.txt").touch() for name in ("LICENSE", "LICENCE", "COPYING", "NOTICE", "AUTHORS"): (temp_dir / name).touch() assert metadata.core.license_files == ["COPYING", "LICENSES/Apache-2.0.txt", "LICENSES/MIT.txt"] class TestAuthors: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": 9000, "dynamic": ["authors"]}}) with pytest.raises( ValueError, match="Metadata field `authors` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.authors def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": "foo"}}) with pytest.raises(TypeError, match="Field `project.authors` must be an array"): _ = metadata.core.authors def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.authors == metadata.core.authors == [] def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": ["foo"]}}) with pytest.raises(TypeError, match="Author #1 of field `project.authors` must be an inline table"): _ = metadata.core.authors def test_no_data(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": [{}]}}) with pytest.raises( ValueError, match="Author #1 of field `project.authors` must specify either `name` or `email`" ): _ = metadata.core.authors def test_name_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": [{"name": 9}]}}) with pytest.raises(TypeError, match="Name of author #1 of field `project.authors` must be a string"): _ = metadata.core.authors def test_name_only(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": [{"name": "foo"}]}}) assert len(metadata.core.authors) == 1 assert metadata.core.authors[0] == {"name": "foo"} assert metadata.core.authors_data == metadata.core.authors_data == {"name": ["foo"], "email": []} def test_email_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": [{"email": 9}]}}) with pytest.raises(TypeError, match="Email of author #1 of field `project.authors` must be a string"): _ = metadata.core.authors def test_email_only(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"authors": [{"email": "foo@bar.baz"}]}}) assert len(metadata.core.authors) == 1 assert metadata.core.authors[0] == {"email": "foo@bar.baz"} assert metadata.core.authors_data == {"name": [], "email": ["foo@bar.baz"]} def test_name_and_email(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": { "authors": [{"name": "foo2", "email": "foo2@bar.baz"}, {"name": "foo1", "email": "foo1@bar.baz"}] } }, ) assert len(metadata.core.authors) == 2 assert metadata.core.authors[0] == {"name": "foo2", "email": "foo2@bar.baz"} assert metadata.core.authors[1] == {"name": "foo1", "email": "foo1@bar.baz"} assert metadata.core.authors_data == {"name": [], "email": ["foo2 <foo2@bar.baz>", "foo1 <foo1@bar.baz>"]} class TestMaintainers: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": 9000, "dynamic": ["maintainers"]}}) with pytest.raises( ValueError, match=( "Metadata field `maintainers` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.maintainers def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": "foo"}}) with pytest.raises(TypeError, match="Field `project.maintainers` must be an array"): _ = metadata.core.maintainers def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.maintainers == metadata.core.maintainers == [] def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": ["foo"]}}) with pytest.raises(TypeError, match="Maintainer #1 of field `project.maintainers` must be an inline table"): _ = metadata.core.maintainers def test_no_data(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": [{}]}}) with pytest.raises( ValueError, match="Maintainer #1 of field `project.maintainers` must specify either `name` or `email`" ): _ = metadata.core.maintainers def test_name_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": [{"name": 9}]}}) with pytest.raises(TypeError, match="Name of maintainer #1 of field `project.maintainers` must be a string"): _ = metadata.core.maintainers def test_name_only(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": [{"name": "foo"}]}}) assert len(metadata.core.maintainers) == 1 assert metadata.core.maintainers[0] == {"name": "foo"} assert metadata.core.maintainers_data == metadata.core.maintainers_data == {"name": ["foo"], "email": []} def test_email_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": [{"email": 9}]}}) with pytest.raises(TypeError, match="Email of maintainer #1 of field `project.maintainers` must be a string"): _ = metadata.core.maintainers def test_email_only(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"maintainers": [{"email": "foo@bar.baz"}]}}) assert len(metadata.core.maintainers) == 1 assert metadata.core.maintainers[0] == {"email": "foo@bar.baz"} assert metadata.core.maintainers_data == {"name": [], "email": ["foo@bar.baz"]} def test_name_and_email(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": { "maintainers": [ {"name": "foo2", "email": "foo2@bar.baz"}, {"name": "foo1", "email": "foo1@bar.baz"}, ] } }, ) assert len(metadata.core.maintainers) == 2 assert metadata.core.maintainers[0] == {"name": "foo2", "email": "foo2@bar.baz"} assert metadata.core.maintainers[1] == {"name": "foo1", "email": "foo1@bar.baz"} assert metadata.core.maintainers_data == {"name": [], "email": ["foo2 <foo2@bar.baz>", "foo1 <foo1@bar.baz>"]} class TestKeywords: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"keywords": 9000, "dynamic": ["keywords"]}}) with pytest.raises( ValueError, match="Metadata field `keywords` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.keywords def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"keywords": 10}}) with pytest.raises(TypeError, match="Field `project.keywords` must be an array"): _ = metadata.core.keywords def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"keywords": [10]}}) with pytest.raises(TypeError, match="Keyword #1 of field `project.keywords` must be a string"): _ = metadata.core.keywords def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"keywords": ["foo", "foo", "bar"]}}) assert metadata.core.keywords == metadata.core.keywords == ["bar", "foo"] class TestClassifiers: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": 9000, "dynamic": ["classifiers"]}}) with pytest.raises( ValueError, match=( "Metadata field `classifiers` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.classifiers def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": 10}}) with pytest.raises(TypeError, match="Field `project.classifiers` must be an array"): _ = metadata.core.classifiers def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": [10]}}) with pytest.raises(TypeError, match="Classifier #1 of field `project.classifiers` must be a string"): _ = metadata.core.classifiers def test_entry_unknown(self, isolation, monkeypatch): monkeypatch.delenv("HATCH_METADATA_CLASSIFIERS_NO_VERIFY", False) metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": ["foo"]}}) with pytest.raises(ValueError, match="Unknown classifier in field `project.classifiers`: foo"): _ = metadata.core.classifiers def test_entry_unknown_no_verify(self, isolation, monkeypatch): monkeypatch.setenv("HATCH_METADATA_CLASSIFIERS_NO_VERIFY", "1") classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.9", "Development Status :: 4 - Beta", "Private :: Do Not Upload", "Foo", ] metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": classifiers}}) assert ( metadata.core.classifiers == metadata.core.classifiers == [ "Private :: Do Not Upload", "Development Status :: 4 - Beta", "Foo", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.11", ] ) def test_correct(self, isolation): classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.9", "Development Status :: 4 - Beta", "Private :: Do Not Upload", ] metadata = ProjectMetadata(str(isolation), None, {"project": {"classifiers": classifiers}}) assert ( metadata.core.classifiers == metadata.core.classifiers == [ "Private :: Do Not Upload", "Development Status :: 4 - Beta", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.11", ] ) class TestURLs: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"urls": 9000, "dynamic": ["urls"]}}) with pytest.raises( ValueError, match="Metadata field `urls` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.urls def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"urls": 10}}) with pytest.raises(TypeError, match="Field `project.urls` must be a table"): _ = metadata.core.urls def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"urls": {"foo": 7}}}) with pytest.raises(TypeError, match="URL `foo` of field `project.urls` must be a string"): _ = metadata.core.urls def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"urls": {"foo": "bar", "bar": "baz"}}}) assert metadata.core.urls == metadata.core.urls == {"bar": "baz", "foo": "bar"} class TestScripts: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"scripts": 9000, "dynamic": ["scripts"]}}) with pytest.raises( ValueError, match="Metadata field `scripts` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.scripts def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"scripts": 10}}) with pytest.raises(TypeError, match="Field `project.scripts` must be a table"): _ = metadata.core.scripts def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"scripts": {"foo": 7}}}) with pytest.raises(TypeError, match="Object reference `foo` of field `project.scripts` must be a string"): _ = metadata.core.scripts def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"scripts": {"foo": "bar", "bar": "baz"}}}) assert metadata.core.scripts == metadata.core.scripts == {"bar": "baz", "foo": "bar"} class TestGUIScripts: def test_dynamic(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"gui-scripts": 9000, "dynamic": ["gui-scripts"]}}) with pytest.raises( ValueError, match=( "Metadata field `gui-scripts` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.gui_scripts def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"gui-scripts": 10}}) with pytest.raises(TypeError, match="Field `project.gui-scripts` must be a table"): _ = metadata.core.gui_scripts def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"gui-scripts": {"foo": 7}}}) with pytest.raises(TypeError, match="Object reference `foo` of field `project.gui-scripts` must be a string"): _ = metadata.core.gui_scripts def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"gui-scripts": {"foo": "bar", "bar": "baz"}}}) assert metadata.core.gui_scripts == metadata.core.gui_scripts == {"bar": "baz", "foo": "bar"} class TestEntryPoints: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"entry-points": 9000, "dynamic": ["entry-points"]}} ) with pytest.raises( ValueError, match=( "Metadata field `entry-points` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.entry_points def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"entry-points": 10}}) with pytest.raises(TypeError, match="Field `project.entry-points` must be a table"): _ = metadata.core.entry_points @pytest.mark.parametrize(("field", "expected"), [("console_scripts", "scripts"), ("gui-scripts", "gui-scripts")]) def test_forbidden_fields(self, isolation, field, expected): metadata = ProjectMetadata(str(isolation), None, {"project": {"entry-points": {field: "foo"}}}) with pytest.raises( ValueError, match=( f"Field `{field}` must be defined as `project.{expected}` instead of " f"in the `project.entry-points` table" ), ): _ = metadata.core.entry_points def test_data_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"entry-points": {"foo": 7}}}) with pytest.raises(TypeError, match="Field `project.entry-points.foo` must be a table"): _ = metadata.core.entry_points def test_data_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"entry-points": {"foo": {"bar": 4}}}}) with pytest.raises( TypeError, match="Object reference `bar` of field `project.entry-points.foo` must be a string" ): _ = metadata.core.entry_points def test_data_empty(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"entry-points": {"foo": {}}}}) assert metadata.core.entry_points == metadata.core.entry_points == {} def test_default(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {}}) assert metadata.core.entry_points == metadata.core.entry_points == {} def test_correct(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"entry-points": {"foo": {"bar": "baz", "foo": "baz"}, "bar": {"foo": "baz", "bar": "baz"}}}}, ) assert ( metadata.core.entry_points == metadata.core.entry_points == {"bar": {"bar": "baz", "foo": "baz"}, "foo": {"bar": "baz", "foo": "baz"}} ) class TestDependencies: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"dependencies": 9000, "dynamic": ["dependencies"]}} ) with pytest.raises( ValueError, match=( "Metadata field `dependencies` cannot be both statically defined and listed in field `project.dynamic`" ), ): _ = metadata.core.dependencies def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dependencies": 10}}) with pytest.raises(TypeError, match="Field `project.dependencies` must be an array"): _ = metadata.core.dependencies def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dependencies": [10]}}) with pytest.raises(TypeError, match="Dependency #1 of field `project.dependencies` must be a string"): _ = metadata.core.dependencies def test_invalid(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"dependencies": ["foo^1"]}}) with pytest.raises(ValueError, match="Dependency #1 of field `project.dependencies` is invalid: .+"): _ = metadata.core.dependencies def test_direct_reference(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"dependencies": ["proj @ git+https://github.com/org/proj.git@v1"]}} ) with pytest.raises( ValueError, match=( "Dependency #1 of field `project.dependencies` cannot be a direct reference unless " "field `tool.hatch.metadata.allow-direct-references` is set to `true`" ), ): _ = metadata.core.dependencies def test_direct_reference_allowed(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": {"dependencies": ["proj @ git+https://github.com/org/proj.git@v1"]}, "tool": {"hatch": {"metadata": {"allow-direct-references": True}}}, }, ) assert metadata.core.dependencies == ["proj @ git+https://github.com/org/proj.git@v1"] def test_context_formatting(self, isolation, uri_slash_prefix): metadata = ProjectMetadata( str(isolation), None, { "project": {"dependencies": ["proj @ {root:uri}"]}, "tool": {"hatch": {"metadata": {"allow-direct-references": True}}}, }, ) normalized_path = str(isolation).replace("\\", "/") assert metadata.core.dependencies == [f"proj @ file:{uri_slash_prefix}{normalized_path}"] def test_correct(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": { "dependencies": [ 'python___dateutil;platform_python_implementation=="CPython"', "bAr.Baz[TLS, Zu.Bat, EdDSA, Zu_Bat] >=1.2RC5 , <9000B1", 'Foo;python_version<"3.8"', 'fOO; python_version< "3.8"', ], }, }, ) assert ( metadata.core.dependencies == metadata.core.dependencies == [ "bar-baz[eddsa,tls,zu-bat]<9000b1,>=1.2rc5", "foo; python_version < '3.8'", "python-dateutil; platform_python_implementation == 'CPython'", ] ) assert metadata.core.dependencies_complex is metadata.core.dependencies_complex class TestOptionalDependencies: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"optional-dependencies": 9000, "dynamic": ["optional-dependencies"]}} ) with pytest.raises( ValueError, match=( "Metadata field `optional-dependencies` cannot be both statically defined and " "listed in field `project.dynamic`" ), ): _ = metadata.core.optional_dependencies def test_not_table(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"optional-dependencies": 10}}) with pytest.raises(TypeError, match="Field `project.optional-dependencies` must be a table"): _ = metadata.core.optional_dependencies def test_invalid_name(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"optional-dependencies": {"foo/bar": []}}}) with pytest.raises( ValueError, match=( "Optional dependency group `foo/bar` of field `project.optional-dependencies` must only contain " "ASCII letters/digits, underscores, hyphens, and periods, and must begin and end with " "ASCII letters/digits." ), ): _ = metadata.core.optional_dependencies def test_definitions_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"optional-dependencies": {"foo": 5}}}) with pytest.raises( TypeError, match="Dependencies for option `foo` of field `project.optional-dependencies` must be an array" ): _ = metadata.core.optional_dependencies def test_entry_not_string(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"optional-dependencies": {"foo": [5]}}}) with pytest.raises( TypeError, match="Dependency #1 of option `foo` of field `project.optional-dependencies` must be a string" ): _ = metadata.core.optional_dependencies def test_invalid(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"optional-dependencies": {"foo": ["bar^1"]}}}) with pytest.raises( ValueError, match="Dependency #1 of option `foo` of field `project.optional-dependencies` is invalid: .+" ): _ = metadata.core.optional_dependencies def test_conflict(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"optional-dependencies": {"foo_bar": [], "foo.bar": []}}} ) with pytest.raises( ValueError, match=( "Optional dependency groups `foo_bar` and `foo.bar` of field `project.optional-dependencies` both " "evaluate to `foo-bar`." ), ): _ = metadata.core.optional_dependencies def test_recursive_circular(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"name": "my-app", "optional-dependencies": {"foo": ["my-app[bar]"], "bar": ["my-app[foo]"]}}}, ) with pytest.raises( ValueError, match="Field `project.optional-dependencies` defines a circular dependency group: foo", ): _ = metadata.core.optional_dependencies def test_recursive_unknown(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"name": "my-app", "optional-dependencies": {"foo": ["my-app[bar]"]}}}, ) with pytest.raises( ValueError, match="Unknown recursive dependency group in field `project.optional-dependencies`: bar", ): _ = metadata.core.optional_dependencies def test_allow_ambiguity(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": {"optional-dependencies": {"foo_bar": [], "foo.bar": []}}, "tool": {"hatch": {"metadata": {"allow-ambiguous-features": True}}}, }, ) assert metadata.core.optional_dependencies == {"foo_bar": [], "foo.bar": []} def test_direct_reference(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"optional-dependencies": {"foo": ["proj @ git+https://github.com/org/proj.git@v1"]}}}, ) with pytest.raises( ValueError, match=( "Dependency #1 of option `foo` of field `project.optional-dependencies` cannot be a direct reference " "unless field `tool.hatch.metadata.allow-direct-references` is set to `true`" ), ): _ = metadata.core.optional_dependencies def test_context_formatting(self, isolation, uri_slash_prefix): metadata = ProjectMetadata( str(isolation), None, { "project": {"name": "my-app", "optional-dependencies": {"foo": ["proj @ {root:uri}"]}}, "tool": {"hatch": {"metadata": {"allow-direct-references": True}}}, }, ) normalized_path = str(isolation).replace("\\", "/") assert metadata.core.optional_dependencies == {"foo": [f"proj @ file:{uri_slash_prefix}{normalized_path}"]} def test_direct_reference_allowed(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": { "name": "my-app", "optional-dependencies": {"foo": ["proj @ git+https://github.com/org/proj.git@v1"]}, }, "tool": {"hatch": {"metadata": {"allow-direct-references": True}}}, }, ) assert metadata.core.optional_dependencies == {"foo": ["proj @ git+https://github.com/org/proj.git@v1"]} def test_correct(self, isolation): metadata = ProjectMetadata( str(isolation), None, { "project": { "name": "my-app", "optional-dependencies": { "foo": [ 'python___dateutil;platform_python_implementation=="CPython"', "bAr.Baz[TLS, Zu.Bat, EdDSA, Zu_Bat] >=1.2RC5 , <9000B1", 'Foo;python_version<"3.8"', 'fOO; python_version< "3.8"', "MY-APP[zZz]", ], "bar": ["foo", "bar", "Baz"], "baz": ["my___app[XYZ]"], "xyz": ["my...app[Bar]"], "zzz": ["aaa"], }, }, }, ) assert ( metadata.core.optional_dependencies == metadata.core.optional_dependencies == { "bar": ["bar", "baz", "foo"], "baz": ["bar", "baz", "foo"], "foo": [ "aaa", "bar-baz[eddsa,tls,zu-bat]<9000b1,>=1.2rc5", "foo; python_version < '3.8'", "python-dateutil; platform_python_implementation == 'CPython'", ], "xyz": ["bar", "baz", "foo"], "zzz": ["aaa"], } ) class TestImportNames: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"import-names": 9000, "dynamic": ["import-names"]}} ) with pytest.raises( ValueError, match="Metadata field `import-names` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.import_names def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-names": 10}}) with pytest.raises(TypeError, match="Field `project.import-names` must be an array"): _ = metadata.core.import_names @pytest.mark.parametrize("entry", [5, "1_foo", "foo.1_bar", "foo ; not-valid"]) def test_entry_not_valid_import_name(self, isolation, entry): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-names": [entry]}}) with pytest.raises( TypeError, match="Import name #1 of field `project.import-names` must be a valid import name" ): _ = metadata.core.import_names def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-names": ["foo", "_foo"]}}) assert metadata.core.import_names == ["_foo", "foo"] def test_private_import_name(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-names": ["foo", "_foo ; private"]}}) assert metadata.core.import_names == ["_foo ; private", "foo"] class TestImportNamespaces: def test_dynamic(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"import-namespaces": 9000, "dynamic": ["import-namespaces"]}} ) with pytest.raises( ValueError, match="Metadata field `import-namespaces` cannot be both statically defined and listed in field `project.dynamic`", ): _ = metadata.core.import_namespaces def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-namespaces": 10}}) with pytest.raises(TypeError, match="Field `project.import-namespaces` must be an array"): _ = metadata.core.import_namespaces @pytest.mark.parametrize("entry", [5, "1_foo", "foo.1_bar", "foo.bar ; not-valid"]) def test_entry_not_valid_import_name(self, isolation, entry): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-namespaces": [entry]}}) with pytest.raises( TypeError, match="Import namespace #1 of field `project.import-namespaces` must be a valid import name" ): _ = metadata.core.import_namespaces def test_correct(self, isolation): metadata = ProjectMetadata(str(isolation), None, {"project": {"import-namespaces": ["foo", "foo.bar"]}}) assert metadata.core.import_namespaces == ["foo", "foo.bar"] def test_private_import_namespace(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"import-namespaces": ["foo", "foo.bar", "foo.bar ; private"]}}, ) assert metadata.core.import_namespaces == ["foo", "foo.bar", "foo.bar ; private"] def test_import_names_and_import_namespaces_conflict(self, isolation): metadata = ProjectMetadata( str(isolation), None, {"project": {"import-names": ["pytest", "foo"], "import-namespaces": ["bar", "pytest"]}}, ) with pytest.raises( ValueError, match="Fields `project.import-names` and `project.import-namespaces` cannot contain the same name", ): _ = metadata.core.import_names metadata2 = ProjectMetadata( str(isolation), None, {"project": {"import-names": ["pytest", "foo"], "import-namespaces": ["bar", "pytest"]}}, ) with pytest.raises( ValueError, match="Fields `project.import-names` and `project.import-namespaces` cannot contain the same name", ): _ = metadata2.core.import_namespaces class TestHook: def test_unknown(self, isolation): metadata = ProjectMetadata( str(isolation), PluginManager(), {"project": {"name": "foo"}, "tool": {"hatch": {"metadata": {"hooks": {"foo": {}}}}}}, ) with pytest.raises(ValueError, match="Unknown metadata hook: foo"): _ = metadata.core def test_custom(self, temp_dir, helpers): classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.9", "Framework :: Foo", "Development Status :: 4 - Beta", "Private :: Do Not Upload", ] metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": {"name": "foo", "classifiers": classifiers, "dynamic": ["version", "description"]}, "tool": {"hatch": {"version": {"path": "a/b"}, "metadata": {"hooks": {"custom": {}}}}}, }, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') file_path = temp_dir / DEFAULT_BUILD_SCRIPT file_path.write_text( helpers.dedent( """ from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomHook(MetadataHookInterface): def update(self, metadata): metadata['description'] = metadata['name'] + 'bar' metadata['version'] = metadata['version'] + 'rc0' def get_known_classifiers(self): return ['Framework :: Foo'] """ ) ) assert "custom" in metadata.hatch.metadata.hooks assert metadata.core.name == "foo" assert metadata.core.description == "foobar" assert metadata.core.version == "0.0.1rc0" assert metadata.core.classifiers == [ "Private :: Do Not Upload", "Development Status :: 4 - Beta", "Framework :: Foo", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.11", ] def test_custom_missing_dynamic(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": {"name": "foo", "dynamic": ["version"]}, "tool": {"hatch": {"version": {"path": "a/b"}, "metadata": {"hooks": {"custom": {}}}}}, }, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') file_path = temp_dir / DEFAULT_BUILD_SCRIPT file_path.write_text( helpers.dedent( """ from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomHook(MetadataHookInterface): def update(self, metadata): metadata['description'] = metadata['name'] + 'bar' """ ) ) with pytest.raises( ValueError, match="The field `description` was set dynamically and therefore must be listed in `project.dynamic`", ): _ = metadata.core class TestHatchPersonalProjectConfigFile: def test_correct(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": {"name": "foo", "dynamic": ["version"]}, "tool": {"hatch": {"build": {"reproducible": False}}}, }, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') (temp_dir / "pyproject.toml").touch() file_path = temp_dir / "hatch.toml" file_path.write_text( helpers.dedent( """ [version] path = 'a/b' """ ) ) assert metadata.version == "0.0.1" assert metadata.hatch.build_config["reproducible"] is False def test_precedence(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": {"name": "foo", "dynamic": ["version"]}, "tool": {"hatch": {"version": {"path": "a/b"}, "build": {"reproducible": False}}}, }, ) file_path = temp_dir / "a" / "b" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.1"') file_path = temp_dir / "c" / "d" file_path.ensure_parent_dir_exists() file_path.write_text('__version__ = "0.0.2"') (temp_dir / "pyproject.toml").touch() file_path = temp_dir / "hatch.toml" file_path.write_text( helpers.dedent( """ [version] path = 'c/d' """ ) ) assert metadata.version == "0.0.2" assert metadata.hatch.build_config["reproducible"] is False class TestMetadataConversion: def test_required_only(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1"} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_dynamic(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "dynamic": ["authors", "classifiers"]} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_description(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "description": "foo bar"} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_urls(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "urls": {"foo": "bar", "bar": "baz"}} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_authors(self, isolation, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "authors": [{"name": "foobar"}, {"email": "bar@domain", "name": "foo"}, {"email": "baz@domain"}], } metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_maintainers(self, isolation, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "maintainers": [{"name": "foobar"}, {"email": "bar@domain", "name": "foo"}, {"email": "baz@domain"}], } metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_keywords(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "keywords": ["bar", "foo"]} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_classifiers(self, isolation, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "classifiers": ["Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.11"], } metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_license_files(self, temp_dir, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "license-files": ["LICENSES/Apache-2.0.txt", "LICENSES/MIT.txt"], } metadata = ProjectMetadata(str(temp_dir), None, {"project": raw_metadata}) licenses_path = temp_dir / "LICENSES" licenses_path.mkdir() licenses_path.joinpath("Apache-2.0.txt").touch() licenses_path.joinpath("MIT.txt").touch() core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_license_expression(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "license": "MIT"} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_license_legacy(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "license": {"text": "foo"}} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_readme(self, isolation, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "readme": {"content-type": "text/markdown", "text": "test content\n"}, } metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_requires_python(self, isolation, latest_spec): raw_metadata = {"name": "My.App", "version": "0.0.1", "requires-python": "<2,>=1"} metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata def test_dependencies(self, isolation, latest_spec): raw_metadata = { "name": "My.App", "version": "0.0.1", "dependencies": ["bar==5", "foo==1"], "optional-dependencies": { "feature1": ['bar==5; python_version < "3"', "foo==1"], "feature2": ["bar==5", 'foo==1; python_version < "3"'], }, } metadata = ProjectMetadata(str(isolation), None, {"project": raw_metadata}) core_metadata = latest_spec(metadata) assert project_metadata_from_core_metadata(core_metadata) == raw_metadata class TestSourceDistributionMetadata: def test_basic_persistence(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), None, { "project": { "name": "My.App", "dynamic": ["version", "keywords"], "dependencies": ["foo==1"], "scripts": {"foo": "bar"}, }, }, ) pkg_info = temp_dir / "PKG-INFO" pkg_info.write_text( helpers.dedent( f""" Metadata-Version: {LATEST_METADATA_VERSION} Name: My.App Version: 0.0.1 Keywords: foo,bar Requires-Dist: bar==5 """ ) ) with temp_dir.as_cwd(): assert metadata.core.config == { "name": "My.App", "version": "0.0.1", "dependencies": ["foo==1"], "keywords": ["foo", "bar"], "scripts": {"foo": "bar"}, "dynamic": [], } def test_metadata_hooks(self, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), PluginManager(), { "project": { "name": "My.App", "dynamic": ["version", "keywords", "description", "scripts"], "dependencies": ["foo==1"], }, "tool": {"hatch": {"metadata": {"hooks": {"custom": {}}}}}, }, ) build_script = temp_dir / DEFAULT_BUILD_SCRIPT build_script.write_text( helpers.dedent( """ from hatchling.metadata.plugin.interface import MetadataHookInterface class CustomHook(MetadataHookInterface): def update(self, metadata): metadata['description'] = metadata['name'] + ' bar' metadata['scripts'] = {'foo': 'bar'} """ ) ) pkg_info = temp_dir / "PKG-INFO" pkg_info.write_text( helpers.dedent( f""" Metadata-Version: {LATEST_METADATA_VERSION} Name: My.App Version: 0.0.1 Summary: My.App bar Keywords: foo,bar Requires-Dist: bar==5 """ ) ) with temp_dir.as_cwd(): assert metadata.core.config == { "name": "My.App", "version": "0.0.1", "description": "My.App bar", "dependencies": ["foo==1"], "keywords": ["foo", "bar"], "scripts": {"foo": "bar"}, "dynamic": ["scripts"], }