pypa-hatch/backend/hatchling/metadata/core.py

1030 lines
37 KiB
Python

import os
import re
import sys
from collections import OrderedDict
from copy import deepcopy
from ..utils.fs import locate_file
# TODO: remove when we drop Python 2
if sys.version_info[0] < 3: # no cov
from io import open
import toml
from ..utils.compat import byteify_object
def load_toml(path):
with open(path, 'r', encoding='utf-8') as f:
# this is to support any `isinstance(metadata_entry, str)`
return byteify_object(toml.loads(f.read()))
else:
import tomli
def load_toml(path):
with open(path, 'r', encoding='utf-8') as f:
return tomli.loads(f.read())
class ProjectMetadata(object):
def __init__(self, root, plugin_manager, config=None):
self.root = root
self.plugin_manager = plugin_manager
self._config = config
self._build = None
self._core = None
self._hatch = None
self._version = None
self._project_file = None
# App already loaded config
if config is not None and root is not None:
self._project_file = os.path.join(root, 'pyproject.toml')
@property
def version(self):
"""
https://www.python.org/dev/peps/pep-0621/#version
"""
if self._version is None:
self._set_version()
return self._version
@property
def config(self):
if self._config is None:
project_file = locate_file(self.root, 'pyproject.toml')
if project_file is None:
self._config = {}
else:
self._project_file = project_file
self._config = load_toml(project_file)
return self._config
@property
def build(self):
if self._build is None:
build_metadata = self.config.get('build-system', {})
if not isinstance(build_metadata, dict):
raise TypeError('The `build-system` configuration must be a table')
self._build = BuildMetadata(self.root, build_metadata)
return self._build
@property
def core(self):
if self._core is None:
if 'project' not in self.config:
raise ValueError('Missing `project` metadata table in configuration')
core_metadata = self.config['project']
if not isinstance(core_metadata, dict):
raise TypeError('The `project` configuration must be a table')
metadata = CoreMetadata(self.root, core_metadata)
metadata_hooks = self.hatch.metadata_hooks
if metadata_hooks:
self._set_version(metadata)
core_metadata['version'] = self.version
for metadata_hook in metadata_hooks.values():
metadata_hook.update(core_metadata)
self._core = metadata
return self._core
@property
def hatch(self):
if self._hatch is None:
tool_config = self.config.get('tool', {})
if not isinstance(tool_config, dict):
raise TypeError('The `tool` configuration must be a table')
hatch_config = tool_config.get('hatch', {})
if not isinstance(hatch_config, dict):
raise TypeError('The `tool.hatch` configuration must be a table')
if self._project_file is not None:
hatch_file = os.path.join(os.path.dirname(self._project_file), 'hatch.toml')
else:
hatch_file = locate_file(self.root, 'hatch.toml')
if hatch_file and os.path.isfile(hatch_file):
config = load_toml(hatch_file)
hatch_config = hatch_config.copy()
hatch_config.update(config)
self._hatch = HatchMetadata(self.root, hatch_config, self.plugin_manager)
return self._hatch
def _set_version(self, core_metadata=None):
if core_metadata is None:
core_metadata = self.core
version = core_metadata.version
if version is None:
version = self.hatch.version
from packaging.utils import canonicalize_version
from packaging.version import Version
self._version = canonicalize_version(Version(version))
class BuildMetadata(object):
"""
https://www.python.org/dev/peps/pep-0517/
"""
def __init__(self, root, config):
self.root = root
self.config = config
self._requires = None
self._requires_complex = None
self._build_backend = None
self._backend_path = None
@property
def requires_complex(self):
if self._requires_complex is None:
from packaging.requirements import InvalidRequirement, Requirement
requires = self.config.get('requires', [])
if not isinstance(requires, list):
raise TypeError('Field `build-system.requires` must be an array')
requires_complex = []
for i, entry in enumerate(requires, 1):
if not isinstance(entry, str):
raise TypeError('Dependency #{} of field `build-system.requires` must be a string'.format(i))
try:
requires_complex.append(Requirement(entry))
except InvalidRequirement as e:
raise ValueError('Dependency #{} of field `build-system.requires` is invalid: {}'.format(i, e))
self._requires_complex = requires_complex
return self._requires_complex
@property
def requires(self):
if self._requires is None:
self._requires = [str(r) for r in self.requires_complex]
return self._requires
@property
def build_backend(self):
if self._build_backend is None:
build_backend = self.config.get('build-backend', '')
if not isinstance(build_backend, str):
raise TypeError('Field `build-system.build-backend` must be a string')
self._build_backend = build_backend
return self._build_backend
@property
def backend_path(self):
if self._backend_path is None:
backend_path = self.config.get('backend-path', [])
if not isinstance(backend_path, list):
raise TypeError('Field `build-system.backend-path` must be an array')
for i, entry in enumerate(backend_path, 1):
if not isinstance(entry, str):
raise TypeError('Entry #{} of field `build-system.backend-path` must be a string'.format(i))
self._backend_path = backend_path
return self._backend_path
class CoreMetadata(object):
"""
https://www.python.org/dev/peps/pep-0621/
"""
def __init__(self, root, config):
self.root = root
self.config = config
self._name = None
self._version = None
self._description = None
self._readme = None
self._readme_content_type = None
self._readme_path = None
self._requires_python = None
self._python_constraint = None
self._license = None
self._license_expression = None
self._license_files = None
self._authors = None
self._authors_data = None
self._maintainers = None
self._maintainers_data = None
self._keywords = None
self._classifiers = None
self._urls = None
self._scripts = None
self._gui_scripts = None
self._entry_points = None
self._dependencies_complex = None
self._dependencies = None
self._optional_dependencies = None
self._dynamic = None
@property
def name(self):
"""
https://www.python.org/dev/peps/pep-0621/#name
"""
if self._name is None:
name = self.config.get('name')
if not name:
raise ValueError('Missing required field `project.name`')
elif not isinstance(name, str):
raise TypeError('Field `project.name` must be a string')
# https://www.python.org/dev/peps/pep-0508/#names
if not re.search('^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', name, re.IGNORECASE):
raise ValueError(
'Required field `project.name` must only contain ASCII letters/digits, '
'underscores, hyphens, and periods.'
)
# https://www.python.org/dev/peps/pep-0503/#normalized-names
self._name = re.sub(r'[-_.]+', '-', name).lower()
return self._name
@property
def version(self):
"""
https://www.python.org/dev/peps/pep-0621/#version
"""
if self._version is None:
if 'version' not in self.config:
if 'version' not in self.dynamic:
raise ValueError(
'Field `project.version` can only be resolved dynamically '
'if `version` is in field `project.dynamic`'
)
else:
version = self.config['version']
if not isinstance(version, str):
raise TypeError('Field `project.version` must be a string')
self._version = version
return self._version
@property
def description(self):
"""
https://www.python.org/dev/peps/pep-0621/#description
"""
if self._description is None:
description = self.config.get('description', '')
if not isinstance(description, str):
raise TypeError('Field `project.description` must be a string')
self._description = description
return self._description
@property
def readme(self):
"""
https://www.python.org/dev/peps/pep-0621/#readme
"""
if self._readme is None:
readme = self.config.get('readme')
if readme is None:
self._readme = ''
self._readme_content_type = 'text/markdown'
self._readme_path = ''
elif isinstance(readme, str):
normalized_path = readme.lower()
if normalized_path.endswith('.md'):
content_type = 'text/markdown'
elif normalized_path.endswith('.rst'):
content_type = 'text/x-rst'
else:
raise TypeError(
'Unable to determine the content-type based on the extension of readme file: {}'.format(readme)
)
readme_path = os.path.normpath(os.path.join(self.root, readme))
if not os.path.isfile(readme_path):
raise OSError('Readme file does not exist: {}'.format(readme))
with open(readme_path, 'r', encoding='utf-8') as f:
self._readme = f.read()
self._readme_content_type = content_type
self._readme_path = readme
elif isinstance(readme, dict):
content_type = readme.get('content-type')
if content_type is None:
raise ValueError('Field `content-type` is required in the `project.readme` table')
elif not isinstance(content_type, str):
raise TypeError('Field `content_type` in the `project.readme` table must be a string')
elif content_type != 'text/markdown' and content_type != 'text/x-rst':
raise ValueError(
'Field `content_type` in the `project.readme` table must be one of the following: '
'text/markdown, text/x-rst'
)
if 'file' in readme and 'text' in readme:
raise ValueError('Cannot specify both `file` and `text` in the `project.readme` table')
if 'file' in readme:
relative_path = readme['file']
if not isinstance(relative_path, str):
raise TypeError('Field `file` in the `project.readme` table must be a string')
path = os.path.normpath(os.path.join(self.root, relative_path))
if not os.path.isfile(path):
raise OSError('Readme file does not exist: {}'.format(relative_path))
with open(path, 'r', encoding=readme.get('charset', 'utf-8')) as f:
contents = f.read()
readme_path = relative_path
elif 'text' in readme:
contents = readme['text']
if not isinstance(contents, str):
raise TypeError('Field `text` in the `project.readme` table must be a string')
readme_path = ''
else:
raise ValueError('Must specify either `file` or `text` in the `project.readme` table')
self._readme = contents
self._readme_content_type = content_type
self._readme_path = readme_path
else:
raise TypeError('Field `project.readme` must be a string or a table')
return self._readme
@property
def readme_content_type(self):
"""
https://www.python.org/dev/peps/pep-0621/#readme
"""
if self._readme_content_type is None:
_ = self.readme
return self._readme_content_type
@property
def readme_path(self):
"""
https://www.python.org/dev/peps/pep-0621/#readme
"""
if self._readme_path is None:
_ = self.readme
return self._readme_path
@property
def requires_python(self):
"""
https://www.python.org/dev/peps/pep-0621/#requires-python
"""
if self._requires_python is None:
from packaging.requirements import InvalidRequirement, Requirement
requires_python = self.config.get('requires-python', '')
if not isinstance(requires_python, str):
raise TypeError('Field `project.requires-python` must be a string')
try:
self._python_constraint = Requirement('requires_python{}'.format(requires_python)).specifier
except InvalidRequirement as e:
raise ValueError('Field `project.requires-python` is invalid: {}'.format(e))
self._requires_python = str(self._python_constraint)
return self._requires_python
@property
def python_constraint(self):
if self._python_constraint is None:
_ = self.requires_python
return self._python_constraint
@property
def license(self):
"""
https://www.python.org/dev/peps/pep-0621/#license
"""
if self._license is None:
data = self.config.get('license')
if data is None:
self._license = ''
self._license_expression = ''
elif isinstance(data, str):
from ..licenses.parse import normalize_license_expression
self._license_expression = normalize_license_expression(data)
self._license = ''
elif isinstance(data, dict):
if 'file' in data and 'text' in data:
raise ValueError('Cannot specify both `file` and `text` in the `project.license` table')
if 'file' in data:
relative_path = data['file']
if not isinstance(relative_path, str):
raise TypeError('Field `file` in the `project.license` table must be a string')
path = os.path.normpath(os.path.join(self.root, relative_path))
if not os.path.isfile(path):
raise OSError('License file does not exist: {}'.format(relative_path))
with open(path, 'r', encoding='utf-8') as f:
contents = f.read()
elif 'text' in data:
contents = data['text']
if not isinstance(contents, str):
raise TypeError('Field `text` in the `project.license` table must be a string')
else:
raise ValueError('Must specify either `file` or `text` in the `project.license` table')
self._license = contents
self._license_expression = ''
else:
raise TypeError('Field `project.license` must be a string or a table')
return self._license
@property
def license_expression(self):
"""
https://www.python.org/dev/peps/pep-0639/
"""
if self._license_expression is None:
_ = self.license
return self._license_expression
@property
def license_files(self):
"""
https://www.python.org/dev/peps/pep-0639/
"""
if self._license_files is None:
if 'license-files' not in self.config:
data = {'globs': ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']}
else:
data = self.config['license-files']
if not isinstance(data, dict):
raise TypeError('Field `project.license-files` must be a table')
elif 'paths' in data and 'globs' in data:
raise ValueError('Cannot specify both `paths` and `globs` in the `project.license-files` table')
license_files = []
if 'paths' in data:
paths = data['paths']
if not isinstance(paths, list):
raise TypeError('Field `paths` in the `project.license-files` table must be an array')
for i, relative_path in enumerate(paths, 1):
if not isinstance(relative_path, str):
raise TypeError(
'Entry #{} in field `paths` in the `project.license-files` table must be a string'.format(i)
)
path = os.path.normpath(os.path.join(self.root, relative_path))
if not os.path.isfile(path):
raise OSError('License file does not exist: {}'.format(relative_path))
license_files.append(os.path.relpath(path, self.root).replace('\\', '/'))
elif 'globs' in data:
from glob import glob
globs = data['globs']
if not isinstance(globs, list):
raise TypeError('Field `globs` in the `project.license-files` table must be an array')
for i, pattern in enumerate(globs, 1):
if not isinstance(pattern, str):
raise TypeError(
'Entry #{} in field `globs` in the `project.license-files` table must be a string'.format(i)
)
full_pattern = os.path.normpath(os.path.join(self.root, pattern))
for path in glob(full_pattern):
if os.path.isfile(path):
license_files.append(os.path.relpath(path, self.root).replace('\\', '/'))
else:
raise ValueError(
'Must specify either `paths` or `globs` in the `project.license-files` table if defined'
)
self._license_files = sorted(license_files)
return self._license_files
@property
def authors(self):
"""
https://www.python.org/dev/peps/pep-0621/#authors-maintainers
"""
if self._authors is None:
authors = self.config.get('authors', [])
if not isinstance(authors, list):
raise TypeError('Field `project.authors` must be an array')
try:
from email.headerregistry import Address
# TODO: remove when we drop Python 2
except ImportError: # no cov
Address = (
lambda display_name='', addr_spec='': addr_spec
if not display_name
else '{} <{}>'.format(display_name, addr_spec)
)
authors = deepcopy(authors)
authors_data = {'name': [], 'email': []}
for i, data in enumerate(authors, 1):
if not isinstance(data, dict):
raise TypeError('Author #{} of field `project.authors` must be an inline table'.format(i))
name = data.get('name', '')
if not isinstance(name, str):
raise TypeError('Name of author #{} of field `project.authors` must be a string'.format(i))
email = data.get('email', '')
if not isinstance(email, str):
raise TypeError('Email of author #{} of field `project.authors` must be a string'.format(i))
if name and email:
authors_data['email'].append(str(Address(display_name=name, addr_spec=email)))
elif email:
authors_data['email'].append(str(Address(addr_spec=email)))
elif name:
authors_data['name'].append(name)
else:
raise ValueError(
'Author #{} of field `project.authors` must specify either `name` or `email`'.format(i)
)
self._authors = authors
self._authors_data = authors_data
return self._authors
@property
def authors_data(self):
"""
https://www.python.org/dev/peps/pep-0621/#authors-maintainers
"""
if self._authors_data is None:
_ = self.authors
return self._authors_data
@property
def maintainers(self):
"""
https://www.python.org/dev/peps/pep-0621/#authors-maintainers
"""
if self._maintainers is None:
maintainers = self.config.get('maintainers', [])
if not isinstance(maintainers, list):
raise TypeError('Field `project.maintainers` must be an array')
try:
from email.headerregistry import Address
# TODO: remove when we drop Python 2
except ImportError: # no cov
Address = (
lambda display_name='', addr_spec='': addr_spec
if not display_name
else '{} <{}>'.format(display_name, addr_spec)
)
maintainers = deepcopy(maintainers)
maintainers_data = {'name': [], 'email': []}
for i, data in enumerate(maintainers, 1):
if not isinstance(data, dict):
raise TypeError('Maintainer #{} of field `project.maintainers` must be an inline table'.format(i))
name = data.get('name', '')
if not isinstance(name, str):
raise TypeError('Name of maintainer #{} of field `project.maintainers` must be a string'.format(i))
email = data.get('email', '')
if not isinstance(email, str):
raise TypeError('Email of maintainer #{} of field `project.maintainers` must be a string'.format(i))
if name and email:
maintainers_data['email'].append(str(Address(display_name=name, addr_spec=email)))
elif email:
maintainers_data['email'].append(str(Address(addr_spec=email)))
elif name:
maintainers_data['name'].append(name)
else:
raise ValueError(
'Maintainer #{} of field `project.maintainers` must specify either `name` or `email`'.format(i)
)
self._maintainers = maintainers
self._maintainers_data = maintainers_data
return self._maintainers
@property
def maintainers_data(self):
"""
https://www.python.org/dev/peps/pep-0621/#authors-maintainers
"""
if self._maintainers_data is None:
_ = self.maintainers
return self._maintainers_data
@property
def keywords(self):
"""
https://www.python.org/dev/peps/pep-0621/#keywords
"""
if self._keywords is None:
keywords = self.config.get('keywords', [])
if not isinstance(keywords, list):
raise TypeError('Field `project.keywords` must be an array')
unique_keywords = set()
for i, keyword in enumerate(keywords, 1):
if not isinstance(keyword, str):
raise TypeError('Keyword #{} of field `project.keywords` must be a string'.format(i))
unique_keywords.add(keyword)
self._keywords = sorted(unique_keywords)
return self._keywords
@property
def classifiers(self):
"""
https://www.python.org/dev/peps/pep-0621/#classifiers
"""
if self._classifiers is None:
classifiers = self.config.get('classifiers', [])
if not isinstance(classifiers, list):
raise TypeError('Field `project.classifiers` must be an array')
unique_classifiers = set()
for i, classifier in enumerate(classifiers, 1):
if not isinstance(classifier, str):
raise TypeError('Classifier #{} of field `project.classifiers` must be a string'.format(i))
unique_classifiers.add(classifier)
self._classifiers = sorted(unique_classifiers)
return self._classifiers
@property
def urls(self):
"""
https://www.python.org/dev/peps/pep-0621/#urls
"""
if self._urls is None:
urls = self.config.get('urls', {})
if not isinstance(urls, dict):
raise TypeError('Field `project.urls` must be a table')
sorted_urls = OrderedDict()
for label, url in sorted(urls.items()):
if not isinstance(url, str):
raise TypeError('URL `{}` of field `project.urls` must be a string'.format(label))
sorted_urls[label] = url
self._urls = sorted_urls
return self._urls
@property
def scripts(self):
"""
https://www.python.org/dev/peps/pep-0621/#entry-points
"""
if self._scripts is None:
scripts = self.config.get('scripts', {})
if not isinstance(scripts, dict):
raise TypeError('Field `project.scripts` must be a table')
sorted_scripts = OrderedDict()
for name, object_ref in sorted(scripts.items()):
if not isinstance(object_ref, str):
raise TypeError('Object reference `{}` of field `project.scripts` must be a string'.format(name))
sorted_scripts[name] = object_ref
self._scripts = sorted_scripts
return self._scripts
@property
def gui_scripts(self):
"""
https://www.python.org/dev/peps/pep-0621/#entry-points
"""
if self._gui_scripts is None:
gui_scripts = self.config.get('gui-scripts', {})
if not isinstance(gui_scripts, dict):
raise TypeError('Field `project.gui-scripts` must be a table')
sorted_gui_scripts = OrderedDict()
for name, object_ref in sorted(gui_scripts.items()):
if not isinstance(object_ref, str):
raise TypeError(
'Object reference `{}` of field `project.gui-scripts` must be a string'.format(name)
)
sorted_gui_scripts[name] = object_ref
self._gui_scripts = sorted_gui_scripts
return self._gui_scripts
@property
def entry_points(self):
"""
https://www.python.org/dev/peps/pep-0621/#entry-points
"""
if self._entry_points is None:
defined_entry_point_groups = self.config.get('entry-points', {})
if not isinstance(defined_entry_point_groups, dict):
raise TypeError('Field `project.entry-points` must be a table')
for forbidden_field in ('scripts', 'gui-scripts'):
if forbidden_field in defined_entry_point_groups:
raise ValueError(
'Field `{0}` must be defined as `project.{0}` instead of in '
'the `project.entry-points` table'.format(forbidden_field)
)
entry_point_groups = OrderedDict()
for group, entry_point_data in sorted(defined_entry_point_groups.items()):
if not isinstance(entry_point_data, dict):
raise TypeError('Field `project.entry-points.{}` must be a table'.format(group))
entry_points = OrderedDict()
for name, object_ref in sorted(entry_point_data.items()):
if not isinstance(object_ref, str):
raise TypeError(
'Object reference `{}` of field `project.entry-points.{}` '
'must be a string'.format(name, group)
)
entry_points[name] = object_ref
if entry_points:
entry_point_groups[group] = entry_points
self._entry_points = entry_point_groups
return self._entry_points
@property
def dependencies_complex(self):
"""
https://www.python.org/dev/peps/pep-0621/#dependencies-optional-dependencies
"""
if self._dependencies_complex is None:
from packaging.requirements import InvalidRequirement, Requirement
dependencies = self.config.get('dependencies', [])
if not isinstance(dependencies, list):
raise TypeError('Field `project.dependencies` must be an array')
dependencies_complex = []
for i, entry in enumerate(dependencies, 1):
if not isinstance(entry, str):
raise TypeError('Dependency #{} of field `project.dependencies` must be a string'.format(i))
try:
dependencies_complex.append(Requirement(entry))
except InvalidRequirement as e:
raise ValueError('Dependency #{} of field `project.dependencies` is invalid: {}'.format(i, e))
self._dependencies_complex = dependencies_complex
return self._dependencies_complex
@property
def dependencies(self):
"""
https://www.python.org/dev/peps/pep-0621/#dependencies-optional-dependencies
"""
if self._dependencies is None:
self._dependencies = sorted(map(str, self.dependencies_complex), key=lambda d: d.lower())
return self._dependencies
@property
def optional_dependencies(self):
"""
https://www.python.org/dev/peps/pep-0621/#dependencies-optional-dependencies
"""
if self._optional_dependencies is None:
from packaging.requirements import InvalidRequirement, Requirement
optional_dependencies = self.config.get('optional-dependencies', {})
if not isinstance(optional_dependencies, dict):
raise TypeError('Field `project.optional-dependencies` must be a table')
optional_dependency_entries = OrderedDict()
for option, dependencies in sorted(optional_dependencies.items()):
if not isinstance(dependencies, list):
raise TypeError(
'Dependencies for option `{}` of field `project.optional-dependencies` '
'must be an array'.format(option)
)
entries = []
for i, entry in enumerate(dependencies, 1):
if not isinstance(entry, str):
raise TypeError(
'Dependency #{} of option `{}` of field `project.optional-dependencies` '
'must be a string'.format(i, option)
)
try:
Requirement(entry)
except InvalidRequirement as e:
raise ValueError(
'Dependency #{} of option `{}` of field `project.optional-dependencies` '
'is invalid: {}'.format(i, option, e)
)
else:
entries.append(entry)
optional_dependency_entries[option] = sorted(entries, key=lambda s: s.lower())
self._optional_dependencies = optional_dependency_entries
return self._optional_dependencies
@property
def dynamic(self):
"""
https://www.python.org/dev/peps/pep-0621/#dynamic
"""
if self._dynamic is None:
dynamic = self.config.get('dynamic', [])
if not isinstance(dynamic, list):
raise TypeError('Field `project.dynamic` must be an array')
if 'name' in dynamic:
raise ValueError('Static metadata field `name` cannot be present in field `project.dynamic`')
unique_fields = set()
for i, field in enumerate(dynamic, 1):
if not isinstance(field, str):
raise TypeError('Field #{} of field `project.dynamic` must be a string'.format(i))
unique_fields.add(field)
self._dynamic = unique_fields
return self._dynamic
class HatchMetadata(object):
def __init__(self, root, config, plugin_manager):
self.root = root
self.config = config
self.plugin_manager = plugin_manager
self._metadata_config = None
self._metadata_hooks = None
self._build_config = None
self._build_targets = None
self._version_source = None
self._version = None
@property
def metadata_config(self):
if self._metadata_config is None:
metadata_config = self.config.get('metadata', {})
if not isinstance(metadata_config, dict):
raise TypeError('Field `tool.hatch.metadata` must be a table')
self._metadata_config = metadata_config
return self._metadata_config
@property
def metadata_hooks(self):
if self._metadata_hooks is None:
hook_config = self.metadata_config
configured_metadata_hooks = OrderedDict()
for hook_name, config in hook_config.items():
metadata_hook = self.plugin_manager.metadata_hook.get(hook_name)
if metadata_hook is None:
raise ValueError('Unknown metadata hook: {}'.format(hook_name))
configured_metadata_hooks[hook_name] = metadata_hook(self.root, config)
self._metadata_hooks = configured_metadata_hooks
return self._metadata_hooks
@property
def build_config(self):
if self._build_config is None:
build_config = self.config.get('build', {})
if not isinstance(build_config, dict):
raise TypeError('Field `tool.hatch.build` must be a table')
self._build_config = build_config
return self._build_config
@property
def build_targets(self):
if self._build_targets is None:
build_targets = self.build_config.get('targets', {})
if not isinstance(build_targets, dict):
raise TypeError('Field `tool.hatch.build.targets` must be a table')
self._build_targets = build_targets
return self._build_targets
@property
def version(self):
if self._version is None:
try:
self._version = self.version_source.get_version_data()['version']
except Exception as e:
raise type(e)(
'Error getting the version from source `{}`: {}'.format(self.version_source.PLUGIN_NAME, e)
) # TODO: from None
return self._version
@property
def version_source(self):
if self._version_source is None:
if 'version' not in self.config:
raise ValueError('Missing `tool.hatch.version` configuration')
options = self.config['version']
if not isinstance(options, dict):
raise TypeError('Field `tool.hatch.version` must be a table')
options = deepcopy(options)
source = options.get('source', 'regex')
if not source:
raise ValueError(
'The `source` option under the `tool.hatch.version` table must not be empty if defined'
)
elif not isinstance(source, str):
raise TypeError('Field `tool.hatch.version.source` must be a string')
version_source = self.plugin_manager.version_source.get(source)
if version_source is None:
raise ValueError('Unknown version source: {}'.format(source))
self._version_source = version_source(self.root, options)
return self._version_source