mirror of https://github.com/pypa/hatch.git
1785 lines
70 KiB
Python
1785 lines
70 KiB
Python
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 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'],
|
|
}
|