pypa-hatch/hatch/config/model.py

756 lines
24 KiB
Python

import os
FIELD_TO_PARSE = object()
class ConfigurationError(Exception):
def __init__(self, *args, location):
self.location = location
super().__init__(*args)
def __str__(self):
return f'Error parsing config:\n{self.location}\n {super().__str__()}'
def parse_config(obj):
if isinstance(obj, LazilyParsedConfig):
obj.parse_fields()
elif isinstance(obj, list):
for o in obj:
parse_config(o)
elif isinstance(obj, dict):
for o in obj.values():
parse_config(o)
class LazilyParsedConfig:
def __init__(self, config: dict, steps: tuple = ()):
self.raw_data = config
self.steps = steps
def parse_fields(self):
for attribute in self.__dict__:
_, prefix, name = attribute.partition('_field_')
if prefix:
parse_config(getattr(self, name))
def raise_error(self, message, *, extra_steps=()):
import inspect
field = inspect.currentframe().f_back.f_code.co_name
raise ConfigurationError(message, location=' -> '.join([*self.steps, field, *extra_steps]))
class RootConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_mode = FIELD_TO_PARSE
self._field_project = FIELD_TO_PARSE
self._field_shell = FIELD_TO_PARSE
self._field_dirs = FIELD_TO_PARSE
self._field_projects = FIELD_TO_PARSE
self._field_publish = FIELD_TO_PARSE
self._field_template = FIELD_TO_PARSE
self._field_terminal = FIELD_TO_PARSE
@property
def mode(self):
if self._field_mode is FIELD_TO_PARSE:
if 'mode' in self.raw_data:
mode = self.raw_data['mode']
if not isinstance(mode, str):
self.raise_error('must be a string')
valid_modes = ('aware', 'local', 'project')
if mode not in valid_modes:
self.raise_error(f'must be one of: {", ".join(valid_modes)}')
self._field_mode = mode
else:
self._field_mode = self.raw_data['mode'] = 'local'
return self._field_mode
@mode.setter
def mode(self, value):
self.raw_data['mode'] = value
self._field_mode = FIELD_TO_PARSE
@property
def project(self):
if self._field_project is FIELD_TO_PARSE:
if 'project' in self.raw_data:
project = self.raw_data['project']
if not isinstance(project, str):
self.raise_error('must be a string')
self._field_project = project
else:
self._field_project = self.raw_data['project'] = ''
return self._field_project
@project.setter
def project(self, value):
self.raw_data['project'] = value
self._field_project = FIELD_TO_PARSE
@property
def shell(self):
if self._field_shell is FIELD_TO_PARSE:
if 'shell' in self.raw_data:
shell = self.raw_data['shell']
if isinstance(shell, str):
self._field_shell = ShellConfig({'name': shell}, ('shell',))
elif isinstance(shell, dict):
self._field_shell = ShellConfig(shell, ('shell',))
else:
self.raise_error('must be a string or table')
else:
self.raw_data['shell'] = ''
self._field_shell = ShellConfig({'name': ''}, ('shell',))
return self._field_shell
@shell.setter
def shell(self, value):
self.raw_data['shell'] = value
self._field_shell = FIELD_TO_PARSE
@property
def dirs(self):
if self._field_dirs is FIELD_TO_PARSE:
if 'dirs' in self.raw_data:
dirs = self.raw_data['dirs']
if not isinstance(dirs, dict):
self.raise_error('must be a table')
self._field_dirs = DirsConfig(dirs, ('dirs',))
else:
dirs = {}
self.raw_data['dirs'] = dirs
self._field_dirs = DirsConfig(dirs, ('dirs',))
return self._field_dirs
@dirs.setter
def dirs(self, value):
self.raw_data['dirs'] = value
self._field_dirs = FIELD_TO_PARSE
@property
def projects(self):
if self._field_projects is FIELD_TO_PARSE:
if 'projects' in self.raw_data:
projects = self.raw_data['projects']
if not isinstance(projects, dict):
self.raise_error('must be a table')
project_data = {}
for name, data in projects.items():
if isinstance(data, str):
project_data[name] = ProjectConfig({'location': data}, ('projects', name))
elif isinstance(data, dict):
project_data[name] = ProjectConfig(data, ('projects', name))
else:
self.raise_error('must be a string or table', extra_steps=(name,))
self._field_projects = project_data
else:
self._field_projects = self.raw_data['projects'] = {}
return self._field_projects
@projects.setter
def projects(self, value):
self.raw_data['projects'] = value
self._field_projects = FIELD_TO_PARSE
@property
def publish(self):
if self._field_publish is FIELD_TO_PARSE:
if 'publish' in self.raw_data:
publish = self.raw_data['publish']
if not isinstance(publish, dict):
self.raise_error('must be a table')
for name, data in publish.items():
if not isinstance(data, dict):
self.raise_error('must be a table', extra_steps=(name,))
self._field_publish = publish
else:
self._field_publish = self.raw_data['publish'] = {'pypi': {'user': '', 'auth': ''}}
return self._field_publish
@publish.setter
def publish(self, value):
self.raw_data['publish'] = value
self._field_publish = FIELD_TO_PARSE
@property
def template(self):
if self._field_template is FIELD_TO_PARSE:
if 'template' in self.raw_data:
template = self.raw_data['template']
if not isinstance(template, dict):
self.raise_error('must be a table')
self._field_template = TemplateConfig(template, ('template',))
else:
template = {}
self.raw_data['template'] = template
self._field_template = TemplateConfig(template, ('template',))
return self._field_template
@template.setter
def template(self, value):
self.raw_data['template'] = value
self._field_template = FIELD_TO_PARSE
@property
def terminal(self):
if self._field_terminal is FIELD_TO_PARSE:
if 'terminal' in self.raw_data:
terminal = self.raw_data['terminal']
if not isinstance(terminal, dict):
self.raise_error('must be a table')
self._field_terminal = TerminalConfig(terminal, ('terminal',))
else:
terminal = {}
self.raw_data['terminal'] = terminal
self._field_terminal = TerminalConfig(terminal, ('terminal',))
return self._field_terminal
@terminal.setter
def terminal(self, value):
self.raw_data['terminal'] = value
self._field_terminal = FIELD_TO_PARSE
class ShellConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_name = FIELD_TO_PARSE
self._field_path = FIELD_TO_PARSE
@property
def name(self):
if self._field_name is FIELD_TO_PARSE:
if 'name' in self.raw_data:
name = self.raw_data['name']
if not isinstance(name, str):
self.raise_error('must be a string')
self._field_name = name
else:
self.raise_error('required field')
return self._field_name
@name.setter
def name(self, value):
self.raw_data['name'] = value
self._field_name = FIELD_TO_PARSE
@property
def path(self):
if self._field_path is FIELD_TO_PARSE:
if 'path' in self.raw_data:
path = self.raw_data['path']
if not isinstance(path, str):
self.raise_error('must be a string')
self._field_path = path
else:
self._field_path = self.raw_data['path'] = self.name
return self._field_path
@path.setter
def path(self, value):
self.raw_data['path'] = value
self._field_path = FIELD_TO_PARSE
class DirsConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_project = FIELD_TO_PARSE
self._field_env = FIELD_TO_PARSE
self._field_python = FIELD_TO_PARSE
self._field_data = FIELD_TO_PARSE
self._field_cache = FIELD_TO_PARSE
@property
def project(self):
if self._field_project is FIELD_TO_PARSE:
if 'project' in self.raw_data:
project = self.raw_data['project']
if not isinstance(project, list):
self.raise_error('must be an array')
for i, entry in enumerate(project, 1):
if not isinstance(entry, str):
self.raise_error('must be a string', extra_steps=(str(i),))
self._field_project = project
else:
self._field_project = self.raw_data['project'] = []
return self._field_project
@project.setter
def project(self, value):
self.raw_data['project'] = value
self._field_project = FIELD_TO_PARSE
@property
def env(self):
if self._field_env is FIELD_TO_PARSE:
if 'env' in self.raw_data:
env = self.raw_data['env']
if not isinstance(env, str):
self.raise_error('must be a string')
self._field_env = env
else:
self._field_env = self.raw_data['env'] = 'isolated'
return self._field_env
@env.setter
def env(self, value):
self.raw_data['env'] = value
self._field_env = FIELD_TO_PARSE
@property
def python(self):
if self._field_python is FIELD_TO_PARSE:
if 'python' in self.raw_data:
python = self.raw_data['python']
if not isinstance(python, str):
self.raise_error('must be a string')
self._field_python = python
else:
self._field_python = self.raw_data['python'] = 'isolated'
return self._field_python
@python.setter
def python(self, value):
self.raw_data['python'] = value
self._field_python = FIELD_TO_PARSE
@property
def data(self):
if self._field_data is FIELD_TO_PARSE:
if 'data' in self.raw_data:
data = self.raw_data['data']
if not isinstance(data, str):
self.raise_error('must be a string')
self._field_data = data
else:
from platformdirs import user_data_dir
self._field_data = self.raw_data['data'] = user_data_dir('hatch', appauthor=False)
return self._field_data
@data.setter
def data(self, value):
self.raw_data['data'] = value
self._field_data = FIELD_TO_PARSE
@property
def cache(self):
if self._field_cache is FIELD_TO_PARSE:
if 'cache' in self.raw_data:
cache = self.raw_data['cache']
if not isinstance(cache, str):
self.raise_error('must be a string')
self._field_cache = cache
else:
from platformdirs import user_cache_dir
self._field_cache = self.raw_data['cache'] = user_cache_dir('hatch', appauthor=False)
return self._field_cache
@cache.setter
def cache(self, value):
self.raw_data['cache'] = value
self._field_cache = FIELD_TO_PARSE
class ProjectConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_location = FIELD_TO_PARSE
@property
def location(self):
if self._field_location is FIELD_TO_PARSE:
if 'location' in self.raw_data:
location = self.raw_data['location']
if not isinstance(location, str):
self.raise_error('must be a string')
self._field_location = location
else:
self.raise_error('required field')
return self._field_location
@location.setter
def location(self, value):
self.raw_data['location'] = value
self._field_location = FIELD_TO_PARSE
class TemplateConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_name = FIELD_TO_PARSE
self._field_email = FIELD_TO_PARSE
self._field_licenses = FIELD_TO_PARSE
self._field_plugins = FIELD_TO_PARSE
@property
def name(self):
if self._field_name is FIELD_TO_PARSE:
if 'name' in self.raw_data:
name = self.raw_data['name']
if not isinstance(name, str):
self.raise_error('must be a string')
self._field_name = name
else:
name = os.environ.get('GIT_AUTHOR_NAME')
if name is None:
import subprocess
try:
name = subprocess.check_output(['git', 'config', '--get', 'user.name'], text=True).strip()
except Exception:
name = 'U.N. Owen'
self._field_name = self.raw_data['name'] = name
return self._field_name
@name.setter
def name(self, value):
self.raw_data['name'] = value
self._field_name = FIELD_TO_PARSE
@property
def email(self):
if self._field_email is FIELD_TO_PARSE:
if 'email' in self.raw_data:
email = self.raw_data['email']
if not isinstance(email, str):
self.raise_error('must be a string')
self._field_email = email
else:
email = os.environ.get('GIT_AUTHOR_EMAIL')
if email is None:
import subprocess
try:
email = subprocess.check_output(['git', 'config', '--get', 'user.email'], text=True).strip()
except Exception:
email = 'void@some.where'
self._field_email = self.raw_data['email'] = email
return self._field_email
@email.setter
def email(self, value):
self.raw_data['email'] = value
self._field_email = FIELD_TO_PARSE
@property
def licenses(self):
if self._field_licenses is FIELD_TO_PARSE:
if 'licenses' in self.raw_data:
licenses = self.raw_data['licenses']
if not isinstance(licenses, dict):
self.raise_error('must be a table')
self._field_licenses = LicensesConfig(licenses, self.steps + ('licenses',))
else:
licenses = {}
self.raw_data['licenses'] = licenses
self._field_licenses = LicensesConfig(licenses, self.steps + ('licenses',))
return self._field_licenses
@licenses.setter
def licenses(self, value):
self.raw_data['licenses'] = value
self._field_licenses = FIELD_TO_PARSE
@property
def plugins(self):
if self._field_plugins is FIELD_TO_PARSE:
if 'plugins' in self.raw_data:
plugins = self.raw_data['plugins']
if not isinstance(plugins, dict):
self.raise_error('must be a table')
for name, data in plugins.items():
if not isinstance(data, dict):
self.raise_error('must be a table', extra_steps=(name,))
self._field_plugins = plugins
else:
self._field_plugins = self.raw_data['plugins'] = {
'default': {'tests': True, 'ci': False, 'src-layout': False}
}
return self._field_plugins
@plugins.setter
def plugins(self, value):
self.raw_data['plugins'] = value
self._field_plugins = FIELD_TO_PARSE
class LicensesConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_headers = FIELD_TO_PARSE
self._field_default = FIELD_TO_PARSE
@property
def headers(self):
if self._field_headers is FIELD_TO_PARSE:
if 'headers' in self.raw_data:
headers = self.raw_data['headers']
if not isinstance(headers, bool):
self.raise_error('must be a boolean')
self._field_headers = headers
else:
self._field_headers = self.raw_data['headers'] = True
return self._field_headers
@headers.setter
def headers(self, value):
self.raw_data['headers'] = value
self._field_headers = FIELD_TO_PARSE
@property
def default(self):
if self._field_default is FIELD_TO_PARSE:
if 'default' in self.raw_data:
default = self.raw_data['default']
if not isinstance(default, list):
self.raise_error('must be an array')
for i, entry in enumerate(default, 1):
if not isinstance(entry, str):
self.raise_error('must be a string', extra_steps=(str(i),))
self._field_default = default
else:
self._field_default = self.raw_data['default'] = ['MIT']
return self._field_default
@default.setter
def default(self, value):
self.raw_data['default'] = value
self._field_default = FIELD_TO_PARSE
class TerminalConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_styles = FIELD_TO_PARSE
@property
def styles(self):
if self._field_styles is FIELD_TO_PARSE:
if 'styles' in self.raw_data:
styles = self.raw_data['styles']
if not isinstance(styles, dict):
self.raise_error('must be a table')
self._field_styles = StylesConfig(styles, self.steps + ('styles',))
else:
styles = {}
self.raw_data['styles'] = styles
self._field_styles = StylesConfig(styles, self.steps + ('styles',))
return self._field_styles
@styles.setter
def styles(self, value):
self.raw_data['styles'] = value
self._field_styles = FIELD_TO_PARSE
class StylesConfig(LazilyParsedConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._field_info = FIELD_TO_PARSE
self._field_success = FIELD_TO_PARSE
self._field_error = FIELD_TO_PARSE
self._field_warning = FIELD_TO_PARSE
self._field_waiting = FIELD_TO_PARSE
self._field_debug = FIELD_TO_PARSE
self._field_spinner = FIELD_TO_PARSE
@property
def info(self):
if self._field_info is FIELD_TO_PARSE:
if 'info' in self.raw_data:
info = self.raw_data['info']
if not isinstance(info, str):
self.raise_error('must be a string')
self._field_info = info
else:
self._field_info = self.raw_data['info'] = 'bold'
return self._field_info
@info.setter
def info(self, value):
self.raw_data['info'] = value
self._field_info = FIELD_TO_PARSE
@property
def success(self):
if self._field_success is FIELD_TO_PARSE:
if 'success' in self.raw_data:
success = self.raw_data['success']
if not isinstance(success, str):
self.raise_error('must be a string')
self._field_success = success
else:
self._field_success = self.raw_data['success'] = 'bold cyan'
return self._field_success
@success.setter
def success(self, value):
self.raw_data['success'] = value
self._field_success = FIELD_TO_PARSE
@property
def error(self):
if self._field_error is FIELD_TO_PARSE:
if 'error' in self.raw_data:
error = self.raw_data['error']
if not isinstance(error, str):
self.raise_error('must be a string')
self._field_error = error
else:
self._field_error = self.raw_data['error'] = 'bold red'
return self._field_error
@error.setter
def error(self, value):
self.raw_data['error'] = value
self._field_error = FIELD_TO_PARSE
@property
def warning(self):
if self._field_warning is FIELD_TO_PARSE:
if 'warning' in self.raw_data:
warning = self.raw_data['warning']
if not isinstance(warning, str):
self.raise_error('must be a string')
self._field_warning = warning
else:
self._field_warning = self.raw_data['warning'] = 'bold yellow'
return self._field_warning
@warning.setter
def warning(self, value):
self.raw_data['warning'] = value
self._field_warning = FIELD_TO_PARSE
@property
def waiting(self):
if self._field_waiting is FIELD_TO_PARSE:
if 'waiting' in self.raw_data:
waiting = self.raw_data['waiting']
if not isinstance(waiting, str):
self.raise_error('must be a string')
self._field_waiting = waiting
else:
self._field_waiting = self.raw_data['waiting'] = 'bold magenta'
return self._field_waiting
@waiting.setter
def waiting(self, value):
self.raw_data['waiting'] = value
self._field_waiting = FIELD_TO_PARSE
@property
def debug(self):
if self._field_debug is FIELD_TO_PARSE:
if 'debug' in self.raw_data:
debug = self.raw_data['debug']
if not isinstance(debug, str):
self.raise_error('must be a string')
self._field_debug = debug
else:
self._field_debug = self.raw_data['debug'] = 'bold'
return self._field_debug
@debug.setter
def debug(self, value):
self.raw_data['debug'] = value
self._field_debug = FIELD_TO_PARSE
@property
def spinner(self):
if self._field_spinner is FIELD_TO_PARSE:
if 'spinner' in self.raw_data:
spinner = self.raw_data['spinner']
if not isinstance(spinner, str):
self.raise_error('must be a string')
self._field_spinner = spinner
else:
self._field_spinner = self.raw_data['spinner'] = 'simpleDotsScrolling'
return self._field_spinner
@spinner.setter
def spinner(self, value):
self.raw_data['spinner'] = value
self._field_spinner = FIELD_TO_PARSE