mirror of https://github.com/pypa/hatch.git
336 lines
10 KiB
Python
336 lines
10 KiB
Python
import re
|
|
import zipfile
|
|
|
|
import pytest
|
|
|
|
from hatchling.builders.custom import CustomBuilder
|
|
from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT
|
|
|
|
|
|
def test_target_config_not_table(isolation):
|
|
config = {'tool': {'hatch': {'build': {'targets': {'custom': 9000}}}}}
|
|
|
|
with pytest.raises(TypeError, match='Field `tool.hatch.build.targets.custom` must be a table'):
|
|
CustomBuilder(str(isolation), config=config)
|
|
|
|
|
|
def test_no_path(isolation):
|
|
config = {
|
|
'tool': {
|
|
'hatch': {
|
|
'build': {'targets': {'custom': {'path': ''}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
with pytest.raises(ValueError, match='Option `path` for builder `custom` must not be empty if defined'):
|
|
CustomBuilder(str(isolation), config=config)
|
|
|
|
|
|
def test_path_not_string(isolation):
|
|
config = {'tool': {'hatch': {'build': {'targets': {'custom': {'path': 3}}}}}}
|
|
|
|
with pytest.raises(TypeError, match='Option `path` for builder `custom` must be a string'):
|
|
CustomBuilder(str(isolation), config=config)
|
|
|
|
|
|
def test_nonexistent(isolation):
|
|
config = {'tool': {'hatch': {'build': {'targets': {'custom': {'path': 'test.py'}}}}}}
|
|
|
|
with pytest.raises(OSError, match='Build script does not exist: test.py'):
|
|
CustomBuilder(str(isolation), config=config)
|
|
|
|
|
|
def test_default(hatch, helpers, temp_dir, config_file):
|
|
config_file.model.template.plugins['default']['src-layout'] = False
|
|
config_file.save()
|
|
|
|
project_name = 'My.App'
|
|
|
|
with temp_dir.as_cwd():
|
|
result = hatch('new', project_name)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
|
|
project_path = temp_dir / 'my-app'
|
|
|
|
config = {
|
|
'project': {'name': project_name, 'dynamic': ['version']},
|
|
'tool': {
|
|
'hatch': {
|
|
'version': {'path': 'my_app/__about__.py'},
|
|
'build': {'targets': {'custom': {}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
file_path = project_path / DEFAULT_BUILD_SCRIPT
|
|
file_path.write_text(
|
|
helpers.dedent(
|
|
"""
|
|
import os
|
|
|
|
from hatchling.builders.wheel import WheelBuilder
|
|
|
|
def get_builder():
|
|
return CustomWheelBuilder
|
|
|
|
class CustomWheelBuilder(WheelBuilder):
|
|
def build(self, **kwargs):
|
|
for i, artifact in enumerate(super().build(**kwargs)):
|
|
build_dir = os.path.dirname(artifact)
|
|
new_path = os.path.join(build_dir, f'{self.PLUGIN_NAME}-{i}.whl')
|
|
os.replace(artifact, new_path)
|
|
yield new_path
|
|
"""
|
|
)
|
|
)
|
|
builder = CustomBuilder(str(project_path), config=config)
|
|
|
|
build_path = project_path / 'dist'
|
|
|
|
with project_path.as_cwd():
|
|
artifacts = list(builder.build())
|
|
|
|
assert len(artifacts) == 1
|
|
expected_artifact = artifacts[0]
|
|
|
|
build_artifacts = list(build_path.iterdir())
|
|
assert len(build_artifacts) == 1
|
|
assert expected_artifact == str(build_artifacts[0])
|
|
assert expected_artifact == str(build_path / 'custom-0.whl')
|
|
|
|
extraction_directory = temp_dir / '_archive'
|
|
extraction_directory.mkdir()
|
|
|
|
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
|
|
zip_archive.extractall(str(extraction_directory))
|
|
|
|
metadata_directory = f'{builder.project_id}.dist-info'
|
|
expected_files = helpers.get_template_files(
|
|
'wheel.standard_default_license_single', project_name, metadata_directory=metadata_directory
|
|
)
|
|
helpers.assert_files(extraction_directory, expected_files)
|
|
|
|
# Inspect the archive rather than the extracted files because on Windows they lose their metadata
|
|
# https://stackoverflow.com/q/9813243
|
|
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
|
|
zip_info = zip_archive.getinfo(f'{metadata_directory}/WHEEL')
|
|
assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)
|
|
|
|
|
|
def test_explicit_path(hatch, helpers, temp_dir, config_file):
|
|
config_file.model.template.plugins['default']['src-layout'] = False
|
|
config_file.save()
|
|
|
|
project_name = 'My.App'
|
|
|
|
with temp_dir.as_cwd():
|
|
result = hatch('new', project_name)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
|
|
project_path = temp_dir / 'my-app'
|
|
|
|
config = {
|
|
'project': {'name': project_name, 'dynamic': ['version']},
|
|
'tool': {
|
|
'hatch': {
|
|
'version': {'path': 'my_app/__about__.py'},
|
|
'build': {'targets': {'custom': {'path': f'foo/{DEFAULT_BUILD_SCRIPT}'}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
file_path = project_path / 'foo' / DEFAULT_BUILD_SCRIPT
|
|
file_path.ensure_parent_dir_exists()
|
|
file_path.write_text(
|
|
helpers.dedent(
|
|
"""
|
|
import os
|
|
|
|
from hatchling.builders.wheel import WheelBuilder
|
|
|
|
def get_builder():
|
|
return CustomWheelBuilder
|
|
|
|
class CustomWheelBuilder(WheelBuilder):
|
|
def build(self, **kwargs):
|
|
for i, artifact in enumerate(super().build(**kwargs)):
|
|
build_dir = os.path.dirname(artifact)
|
|
new_path = os.path.join(build_dir, f'{self.PLUGIN_NAME}-{i}.whl')
|
|
os.replace(artifact, new_path)
|
|
yield new_path
|
|
"""
|
|
)
|
|
)
|
|
builder = CustomBuilder(str(project_path), config=config)
|
|
|
|
build_path = project_path / 'dist'
|
|
|
|
with project_path.as_cwd():
|
|
artifacts = list(builder.build())
|
|
|
|
assert len(artifacts) == 1
|
|
expected_artifact = artifacts[0]
|
|
|
|
build_artifacts = list(build_path.iterdir())
|
|
assert len(build_artifacts) == 1
|
|
assert expected_artifact == str(build_artifacts[0])
|
|
assert expected_artifact == str(build_path / 'custom-0.whl')
|
|
|
|
extraction_directory = temp_dir / '_archive'
|
|
extraction_directory.mkdir()
|
|
|
|
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
|
|
zip_archive.extractall(str(extraction_directory))
|
|
|
|
metadata_directory = f'{builder.project_id}.dist-info'
|
|
expected_files = helpers.get_template_files(
|
|
'wheel.standard_default_license_single', project_name, metadata_directory=metadata_directory
|
|
)
|
|
helpers.assert_files(extraction_directory, expected_files)
|
|
|
|
# Inspect the archive rather than the extracted files because on Windows they lose their metadata
|
|
# https://stackoverflow.com/q/9813243
|
|
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
|
|
zip_info = zip_archive.getinfo(f'{metadata_directory}/WHEEL')
|
|
assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)
|
|
|
|
|
|
def test_no_subclass(hatch, helpers, temp_dir):
|
|
project_name = 'My.App'
|
|
|
|
with temp_dir.as_cwd():
|
|
result = hatch('new', project_name)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
|
|
project_path = temp_dir / 'my-app'
|
|
|
|
config = {
|
|
'project': {'name': project_name, 'dynamic': ['version']},
|
|
'tool': {
|
|
'hatch': {
|
|
'version': {'path': 'my_app/__about__.py'},
|
|
'build': {'targets': {'custom': {'path': f'foo/{DEFAULT_BUILD_SCRIPT}'}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
file_path = project_path / 'foo' / DEFAULT_BUILD_SCRIPT
|
|
file_path.ensure_parent_dir_exists()
|
|
file_path.write_text(
|
|
helpers.dedent(
|
|
"""
|
|
from hatchling.builders.plugin.interface import BuilderInterface
|
|
|
|
foo = None
|
|
bar = 'baz'
|
|
|
|
class CustomBuilder:
|
|
pass
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=re.escape(f'Unable to find a subclass of `BuilderInterface` in `foo/{DEFAULT_BUILD_SCRIPT}`: {temp_dir}'),
|
|
), project_path.as_cwd():
|
|
CustomBuilder(str(project_path), config=config)
|
|
|
|
|
|
def test_multiple_subclasses(hatch, helpers, temp_dir):
|
|
project_name = 'My.App'
|
|
|
|
with temp_dir.as_cwd():
|
|
result = hatch('new', project_name)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
|
|
project_path = temp_dir / 'my-app'
|
|
|
|
config = {
|
|
'project': {'name': project_name, 'dynamic': ['version']},
|
|
'tool': {
|
|
'hatch': {
|
|
'version': {'path': 'my_app/__about__.py'},
|
|
'build': {'targets': {'custom': {'path': f'foo/{DEFAULT_BUILD_SCRIPT}'}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
file_path = project_path / 'foo' / DEFAULT_BUILD_SCRIPT
|
|
file_path.ensure_parent_dir_exists()
|
|
file_path.write_text(
|
|
helpers.dedent(
|
|
"""
|
|
import os
|
|
|
|
from hatchling.builders.wheel import WheelBuilder
|
|
|
|
class CustomWheelBuilder(WheelBuilder):
|
|
pass
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=re.escape(
|
|
f'Multiple subclasses of `BuilderInterface` found in `foo/{DEFAULT_BUILD_SCRIPT}`, select '
|
|
f'one by defining a function named `get_builder`: {temp_dir}'
|
|
),
|
|
), project_path.as_cwd():
|
|
CustomBuilder(str(project_path), config=config)
|
|
|
|
|
|
def test_dynamic_dependencies(hatch, helpers, temp_dir, config_file):
|
|
config_file.model.template.plugins['default']['src-layout'] = False
|
|
config_file.save()
|
|
|
|
project_name = 'My.App'
|
|
|
|
with temp_dir.as_cwd():
|
|
result = hatch('new', project_name)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
|
|
project_path = temp_dir / 'my-app'
|
|
|
|
config = {
|
|
'project': {'name': project_name, 'dynamic': ['version']},
|
|
'tool': {
|
|
'hatch': {
|
|
'version': {'path': 'my_app/__about__.py'},
|
|
'build': {
|
|
'targets': {'custom': {'dependencies': ['foo'], 'hooks': {'custom': {'dependencies': ['bar']}}}}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
file_path = project_path / DEFAULT_BUILD_SCRIPT
|
|
file_path.write_text(
|
|
helpers.dedent(
|
|
"""
|
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
from hatchling.builders.wheel import WheelBuilder
|
|
|
|
def get_builder():
|
|
return CustomWheelBuilder
|
|
|
|
class CustomWheelBuilder(WheelBuilder):
|
|
pass
|
|
|
|
class CustomHook(BuildHookInterface):
|
|
def dependencies(self):
|
|
return ['baz']
|
|
"""
|
|
)
|
|
)
|
|
builder = CustomBuilder(str(project_path), config=config)
|
|
|
|
assert builder.config.dependencies == ['foo', 'bar', 'baz']
|