Added backgrounds format. fixed #75

master
Иванов Денис 3 years ago
parent d6c944890c
commit e59a4511d6

@ -0,0 +1,318 @@
# vim: fileencoding=utf-8
#
from ..template_engine import ParametersContainer, Variables
from ...variables.datavars import NamespaceNode, VariableNotFoundError
from ...variables.loader import Datavars
from ...utils.images import ImageMagick
from .base_format import Format, FormatError
from typing import Union, List, Tuple, NoReturn
import hashlib
import re
import os
class BackgroundsFormat(Format):
FORMAT = 'backgrounds'
EXECUTABLE = True
FORMAT_PARAMETERS = {'convert', 'stretch'}
def __init__(self, template_text: str,
template_path: str,
parameters: ParametersContainer,
datavars: Union[Datavars, NamespaceNode, Variables],
chroot_path: str = "/"):
self._lines: List[str] = [line for line
in template_text.strip().split('\n') if line]
self._datavars: Union[Datavars, NamespaceNode, Variables] = datavars
if parameters.source:
self._source = parameters.source
else:
raise FormatError("source parameter is not set for template with"
"with 'backgrounds' format.")
self._mirror: bool = parameters.mirror
self._stretch: bool = parameters.stretch
self._convert: Union[bool, str] = parameters.convert
self._empty_name: bool = (parameters.name == '')
# Измененные файлы.
self.changed_files: dict = dict()
# Список предупреждений.
self._warnings: list = []
# Флаг для тестов.
self._fake_chroot: bool = False
try:
self._fake_chroot = datavars.main.fake_chroot
except Exception:
pass
def execute_format(self, target_path: str, chroot_path: str = '/') -> dict:
'''Метод для запуска работы формата.'''
if not self._check_source(self._source, target_path, self._mirror):
return self.changed_files
print(f"FAKE_CHROOT: {self._fake_chroot}")
self._magician = ImageMagick(
chroot=chroot_path if not self._fake_chroot else "/")
source_resolution = self._magician.get_image_resolution(self._source)
resolutions_from_var = False
resolutions = self._get_resolutions_from_lines(self._lines,
source_resolution)
if not resolutions:
resolutions = self._get_resolutions_from_var(self._datavars)
resolutions_from_var = True
action_md5sum = self._get_action_md5sum(self._source, resolutions)
if not self._check_target_directory(target_path, self._mirror,
resolutions, action_md5sum):
return {}
images_format = self._get_format_value(self._convert,
self._datavars)
if self._convert and self._convert == "GFXBOOT":
converter = self._magician.convert_resize_gfxboot
else:
converter = self._magician.convert_resize_crop_center
output_paths = self._make_output_paths(self._lines, images_format,
target_path, resolutions,
resolutions_from_var)
# Выполняем преобразование изображений в требуемый размер и формат.
for resolution, output_path in output_paths.items():
if not self._check_stretch(source_resolution,
resolution,
self._stretch):
continue
width, height = resolution
converter(self._source, output_path, height, width,
image_format=images_format)
if output_path in self.changed_files:
self.changed_files[output_path] = 'M'
else:
self.changed_files[output_path] = 'N'
self._create_md5sum_file(target_path, action_md5sum)
return self.changed_files
def _get_format_value(self, convert: Union[bool, str],
datavars: Union[Datavars, NamespaceNode, Variables]
) -> str:
"""Метод для получения значения формата."""
if convert:
if convert == "JPG" or convert == "GFXBOOT":
images_format = "JPEG"
else:
images_format = convert
else:
images_format = self._magician.get_image_format(self._source)
return images_format
def _make_output_paths(self, lines: List[str],
images_format: str,
target_path: str,
resolutions: List[Tuple[int, int]],
resolutions_from_var: bool
) -> List[str]:
"""Метод для получения списка путей, по которым будут создаваться
изображения."""
if not self._empty_name:
path, name = os.path.split(target_path)
else:
path = os.path.dirname(target_path)
name = ''
paths = {}
if not resolutions_from_var and len(resolutions) == 1:
if self._empty_name:
raise FormatError("'name' parameter is empty in 'backgrounds'"
" format template with single file output.")
paths = {next(iter(resolutions)): target_path}
return paths
for width, height in resolutions:
paths[(width, height)] = (f"{path}/{name}{width}x{height}"
f".{images_format.lower()}")
return paths
def _get_resolutions_from_lines(self, lines: List[str],
source_resolution: Tuple[int, int]
) -> List[Tuple[int, int]]:
"""Метод для получения списка кортежей с разрешениями выходных
изображений из текста шаблона."""
resolutions = []
if lines:
for line in lines:
if (line.strip() == "original"
and source_resolution not in resolutions):
resolutions.append(source_resolution)
else:
try:
width, height = line.lower().strip().split("x")
resolution = (int(width), int(height))
except Exception:
raise FormatError("can not parse line from template"
" with 'backgrounds' format:"
f" '{line}'")
if resolution not in resolutions:
resolutions.append(resolution)
return resolutions
def _get_resolutions_from_var(self, datavars: Union[Datavars,
NamespaceNode,
Variables]
) -> List[Tuple[int, int]]:
"""Метод для получения списка кортежей с разрешениями выходных
изображений из переменной."""
try:
resolutions = []
for resolution in self._datavars.main.cl_resolutions:
resolution = tuple(resolution.strip().split("x"))
resolutions.append(resolution)
return resolutions
except VariableNotFoundError:
raise FormatError("resolutions values was not found.")
except Exception:
raise FormatError("can not use resolution values from variable:"
" 'main.cl_resolutions'")
def _check_target_directory(self, target_path: str, mirror: bool,
resolutions: List[Tuple[int, int]],
action_md5sum: str
) -> Union[bool, str]:
"""Метод для проверки содержимого целевой директории, удаления
изображений и md5-сумм, подлежащих удалению, а также сравнения
имеющейся в директории md5-суммы с суммой, полученной для действия
текущего шаблона."""
if not self._empty_name:
path, name = os.path.split(target_path)
else:
path = os.path.dirname(target_path)
name = ''
name_pattern = re.compile(rf"^{name}\d+x\d+")
md5sum = None
images = []
# Проверяем содержимое целевой директории.
for node in os.scandir(path):
if node.is_file():
if (node.name ==
f"{name.strip('-_.')}{'.' if name else ''}md5sum"):
md5sum = node.name
elif (node.name == name
or (name_pattern.search(node.name) is not None)):
images.append(node.name)
if not images and md5sum is None:
# Если нет файла суммы и нет изображений -- продолжаем выполнение
# шаблона.
return True
elif not images and md5sum is not None:
# Если есть файл суммы, но нет изображений -- удаляем файл суммы.
md5sum_path = os.path.join(path, md5sum)
os.unlink(md5sum_path)
self.changed_files[md5sum_path] = 'D'
return True
elif images and md5sum is None:
# Если есть файлы изображений, но нет суммы -- удаляем файлы
# изображений.
for image in images:
image_path = os.path.join(path, image)
os.unlink(image_path)
self.changed_files[image_path] = 'D'
return True
else:
# Сравниваем суммы из md5sum и для текущего действия если они
# сходятся, то делать ничего не надо, если нет -- удаляем
# имеющиеся изображения и md5-сумму.
with open(os.path.join(path, md5sum), "r") as md5sum_file:
current_md5sum = md5sum_file.read().strip()
if current_md5sum != action_md5sum:
for image in images:
image_path = os.path.join(path, image)
os.unlink(image_path)
self.changed_files[image_path] = 'D'
md5sum_path = os.path.join(path, md5sum)
os.unlink(md5sum_path)
self.changed_files[md5sum_path] = 'D'
return True
else:
return False
def _check_source(self, source: str, target_path: str, mirror: bool
) -> bool:
"""Метод для проверки исходного изображения."""
if not self._empty_name:
path, name = os.path.split(target_path)
else:
path = os.path.dirname(target_path)
name = ''
name_pattern = re.compile(rf"^{name}\d+x\d+")
if not os.path.exists(source):
if mirror:
for node in os.scandir(path):
if (node.name == name
or name_pattern.search(node.name) is not None
or node.name == (f"{node.name.strip('_-.')}"
f"{ '.' if name else '' }md5sum")):
os.unlink(node.path)
self.changed_files[node.path] = "D"
return False
else:
raise FormatError("image from 'source' parameter does not"
f" exist: {source}.")
return True
def _get_action_md5sum(self, source: str,
resolutions: List[Tuple[int, int]]) -> bool:
"""Метод для получения md5-суммы текущего действия шаблона,
рассчитываемой из последовательности байтов изображения и списка
разрешений, в которые данный файл должен быть конвертирован."""
print("RESOLUTIONS:", resolutions)
with open(source, "rb") as source_file:
md5_object = hashlib.md5(source_file.read())
for width, height in resolutions:
resolution = f"{width}x{height}"
md5_object.update(resolution.encode())
return md5_object.hexdigest()
def _create_md5sum_file(self, target_path: str, action_md5sum: str
) -> NoReturn:
"""Метод для создания файла с md5-суммой действия, выполненного
данным шаблоном."""
if not self._empty_name:
path, name = os.path.split(target_path)
else:
path = os.path.dirname(target_path)
name = ''
md5sum_path = (f"{path}/{name.strip('_-.')}"
f"{ '.' if name else '' }md5sum")
with open(md5sum_path, "w") as md5sum_file:
md5sum_file.write(action_md5sum)
if md5sum_path in self.changed_files:
self.changed_files[md5sum_path] = 'M'
else:
self.changed_files[md5sum_path] = 'N'
def _check_stretch(self, source_resolution: Tuple[int, int],
resolution: Tuple[int, int],
stretch: bool) -> bool:
"""Метод определяющий необходимость растягивания исходного изображения
и делающий вывод о том, возможно ли создание изображения заданного
разрешения исходя из значения параметра stretch."""
return (stretch
or (source_resolution[0] >= resolution[0]
and source_resolution[1] >= resolution[1]))
@property
def warnings(self):
return self._warnings

@ -2,11 +2,15 @@
# #
import os import os
from .base_format import Format, FormatError from .base_format import Format, FormatError
from calculate.utils.files import join_paths
from pyparsing import Literal, Regex, SkipTo, LineEnd, lineno, LineStart from pyparsing import Literal, Regex, SkipTo, LineEnd, lineno, LineStart
from calculate.utils.package import PackageAtomParser, Package, PackageNotFound from calculate.utils.package import PackageAtomParser, Package, PackageNotFound
from calculate.utils.files import join_paths from ..template_engine import ParametersContainer, Variables
from glob import iglob from ...variables.datavars import NamespaceNode
from ...variables.loader import Datavars
from fnmatch import fnmatch from fnmatch import fnmatch
from typing import Union
from glob import iglob
ADD, REMOVE, MOVE = range(0, 3) ADD, REMOVE, MOVE = range(0, 3)
@ -25,7 +29,8 @@ class ContentsFormat(Format):
def __init__(self, template_text: str, def __init__(self, template_text: str,
template_path: str, template_path: str,
ignore_comments=None): parameters: ParametersContainer,
datavars: Union[Datavars, NamespaceNode, Variables]):
self._command_methods = {ADD: self._add_command, self._command_methods = {ADD: self._add_command,
REMOVE: self._remove_command, REMOVE: self._remove_command,
MOVE: self._move_command} MOVE: self._move_command}
@ -39,7 +44,10 @@ class ContentsFormat(Format):
self._packages = dict() self._packages = dict()
self._atom_parser = None self._atom_parser = None
def execute_format(self, target_path, chroot_path='/'): # Предупреждения.
self._warnings: list = []
def execute_format(self, target_path: str, chroot_path='/') -> dict:
'''Метод для запуска работы формата.''' '''Метод для запуска работы формата.'''
self._package = dict() self._package = dict()
self._atom_parser = PackageAtomParser(chroot_path=chroot_path) self._atom_parser = PackageAtomParser(chroot_path=chroot_path)
@ -215,3 +223,7 @@ class ContentsFormat(Format):
if not result: if not result:
return [None] return [None]
return {'error': result, 'lineno': lineno(location, string)} return {'error': result, 'lineno': lineno(location, string)}
@property
def warnings(self):
return self._warnings

@ -3,6 +3,10 @@
from .base_format import Format from .base_format import Format
from calculate.utils.files import Process from calculate.utils.files import Process
from calculate.templates.format.base_format import FormatError from calculate.templates.format.base_format import FormatError
from ..template_engine import ParametersContainer, Variables
from ...variables.datavars import NamespaceNode
from ...variables.loader import Datavars
from typing import Union
from os import path from os import path
@ -12,7 +16,8 @@ class PatchFormat(Format):
def __init__(self, patch_text: str, def __init__(self, patch_text: str,
template_path: str, template_path: str,
ignore_comments=None): parameters: ParametersContainer,
datavars: Union[Datavars, NamespaceNode, Variables]):
self._patch_text = patch_text self._patch_text = patch_text
self._cwd_path = '/' self._cwd_path = '/'
self._last_level = 0 self._last_level = 0
@ -20,6 +25,9 @@ class PatchFormat(Format):
# Измененные файлы. # Измененные файлы.
self.changed_files = dict() self.changed_files = dict()
# Предупреждения.
self._warnings: list = []
def execute_format(self, target_path, chroot_path='/'): def execute_format(self, target_path, chroot_path='/'):
'''Метод для запуска работы формата.''' '''Метод для запуска работы формата.'''
self._cwd_path = target_path self._cwd_path = target_path
@ -81,3 +89,7 @@ class PatchFormat(Format):
def __bool__(self): def __bool__(self):
return bool(self._patch_text) return bool(self._patch_text)
@property
def warnings(self):
return self._warnings

@ -1,12 +1,15 @@
# vim: fileencoding=utf-8 # vim: fileencoding=utf-8
# #
from jinja2.ext import Extension from jinja2.ext import Extension
from jinja2.lexer import Token
from jinja2.parser import Parser
from jinja2 import ( from jinja2 import (
Environment, Environment,
FileSystemLoader, FileSystemLoader,
TemplateSyntaxError, TemplateSyntaxError,
nodes, nodes,
contextfunction contextfunction,
Template,
) )
from jinja2.utils import missing from jinja2.utils import missing
from jinja2.runtime import Context, Undefined from jinja2.runtime import Context, Undefined
@ -22,7 +25,10 @@ from typing import (
Union, Union,
Any, Any,
List, List,
Tuple Tuple,
NoReturn,
Optional,
Iterator,
) )
from ..utils.package import ( from ..utils.package import (
PackageAtomName, PackageAtomName,
@ -39,6 +45,7 @@ from ..utils.files import (
FilesError FilesError
) )
from calculate.variables.datavars import ( from calculate.variables.datavars import (
VariableNotFoundError,
HashType, HashType,
NamespaceNode, NamespaceNode,
VariableNode, VariableNode,
@ -55,7 +62,6 @@ import calculate.templates.template_filters as template_filters
# Типы шаблона: директория или файл. # Типы шаблона: директория или файл.
DIR, FILE, LINK = range(3) DIR, FILE, LINK = range(3)
# Словарь, в котором можно регистрировать фильтры. # Словарь, в котором можно регистрировать фильтры.
CALCULATE_FILTERS = {"cut": template_filters.cut} CALCULATE_FILTERS = {"cut": template_filters.cut}
@ -79,19 +85,23 @@ class ConditionFailed(TemplateSyntaxError):
class Variables(MutableMapping): class Variables(MutableMapping):
'''Класс-заглушка вместо модуля переменных для тестов.''' '''Класс-заглушка вместо модуля переменных для тестов.'''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.__attrs = dict(*args, **kwargs) self.__attrs: dict = dict(*args, **kwargs)
self.__iter: Union[Iterator, None] = None
def __next__(self): def __next__(self) -> Any:
iterator = iter(self.__attrs) if self._iter is None:
return next(iterator) self._iter = iter(self.__attrs)
return next(self._iter)
def __getattribute__(self, name: str): def __getattribute__(self, name: str) -> Any:
if name == '_Variables__attrs': if name == '_Variables__attrs':
return super().__getattribute__(name) return super().__getattribute__(name)
if name == 'available_packages': if name == 'available_packages':
return super().__getattribute__(name) return super().__getattribute__(name)
if name == '_variables': if name == '_variables':
return self.__attrs return self.__attrs
if name == '_iter':
return self._iter
try: try:
return self.__attrs[name] return self.__attrs[name]
except KeyError: except KeyError:
@ -103,16 +113,16 @@ class Variables(MutableMapping):
packages.update({'custom'}) packages.update({'custom'})
return packages return packages
def __getitem__(self, name: str): def __getitem__(self, name: str) -> Any:
return self.__attrs[name] return self.__attrs[name]
def __setitem__(self, name: str, value) -> None: def __setitem__(self, name: str, value: Any) -> NoReturn:
self.__attrs[name] = value self.__attrs[name] = value
def __delitem__(self, name: str) -> None: def __delitem__(self, name: str) -> NoReturn:
del self.__attrs[name] del self.__attrs[name]
def __iter__(self): def __iter__(self) -> Iterator:
return iter(self.__attrs) return iter(self.__attrs)
def __len__(self) -> int: def __len__(self) -> int:
@ -124,7 +134,7 @@ class Variables(MutableMapping):
def __contains__(self, name: str) -> bool: def __contains__(self, name: str) -> bool:
return name in self.__attrs return name in self.__attrs
def __hash__(self): def __hash__(self) -> int:
return hash(id(self)) return hash(id(self))
@ -135,7 +145,8 @@ class ParametersProcessor:
'format', 'unbound', 'mirror', 'run', 'exec', 'format', 'unbound', 'mirror', 'run', 'exec',
'env', 'package', 'merge', 'postmerge', 'env', 'package', 'merge', 'postmerge',
'action', 'rebuild', 'restart', 'stop', 'action', 'rebuild', 'restart', 'stop',
'start', 'handler', 'notify', 'group'} 'start', 'handler', 'notify', 'group',
'convert', 'stretch'}
inheritable_parameters: set = {'chmod', 'chown', 'autoupdate', 'env', inheritable_parameters: set = {'chmod', 'chown', 'autoupdate', 'env',
'package', 'action', 'handler', 'group'} 'package', 'action', 'handler', 'group'}
@ -158,21 +169,24 @@ class ParametersProcessor:
r'([r-][w-][Xx-])([r-][w-][Xx-])([r-][w-][Xx-])') r'([r-][w-][Xx-])([r-][w-][Xx-])([r-][w-][Xx-])')
def __init__(self, def __init__(self,
parameters_container: Union["ParametersContainer", parameters_container: Optional["ParametersContainer"] = None,
None] = None,
chroot_path: str = '/', chroot_path: str = '/',
datavars_module: Union[Datavars, Variables] = Variables(), datavars_module: Union[Datavars,
for_package: Union[Package, None] = None): NamespaceNode,
Variables] = Variables(),
for_package: Optional[Package] = None):
self.chroot_path: str = chroot_path self.chroot_path: str = chroot_path
self.template_type: int = DIR self.template_type: int = DIR
self.datavars_module: Union[Datavars, Variables] = datavars_module self.datavars_module: Union[Datavars,
NamespaceNode,
Variables] = datavars_module
self._parameters_container: ParametersContainer = parameters_container self._parameters_container: ParametersContainer = parameters_container
self.package_atom_parser: PackageAtomParser =\ self.package_atom_parser: PackageAtomParser = PackageAtomParser(
PackageAtomParser(chroot_path=chroot_path) chroot_path=chroot_path)
self._groups: dict = {} self._groups: dict = {}
try: try:
@ -212,7 +226,9 @@ class ParametersProcessor:
'merge': self.check_merge_parameter, 'merge': self.check_merge_parameter,
'format': self.check_format_parameter, 'format': self.check_format_parameter,
'handler': self.check_handler_parameter, 'handler': self.check_handler_parameter,
'notify': self.check_notify_parameter 'notify': self.check_notify_parameter,
'convert': self.check_convert_parameter,
'stretch': self.check_stretch_parameter,
}) })
# Если добавляемый параметр должен быть проверен после того, как # Если добавляемый параметр должен быть проверен после того, как
@ -225,7 +241,9 @@ class ParametersProcessor:
'autoupdate': self.check_postparse_autoupdate, 'autoupdate': self.check_postparse_autoupdate,
'run': self.check_postparse_run, 'run': self.check_postparse_run,
'exec': self.check_postparse_exec, 'exec': self.check_postparse_exec,
'handler': self.check_postparse_handler 'handler': self.check_postparse_handler,
'convert': self.check_postparse_convert,
'stretch': self.check_postparse_stretch,
}) })
# Если параметр является наследуемым только при некоторых условиях -- # Если параметр является наследуемым только при некоторых условиях --
@ -234,7 +252,7 @@ class ParametersProcessor:
def set_parameters_container(self, def set_parameters_container(self,
parameters_container: "ParametersContainer" parameters_container: "ParametersContainer"
) -> None: ) -> NoReturn:
'''Метод для установки текущего контейнера параметров.''' '''Метод для установки текущего контейнера параметров.'''
self._parameters_container = parameters_container self._parameters_container = parameters_container
self._added_parameters = set() self._added_parameters = set()
@ -244,7 +262,7 @@ class ParametersProcessor:
return self._for_package return self._for_package
@for_package.setter @for_package.setter
def for_package(self, package: Package): def for_package(self, package: Package) -> NoReturn:
self._for_package = package self._for_package = package
def __getattr__(self, parameter_name: str) -> Any: def __getattr__(self, parameter_name: str) -> Any:
@ -258,7 +276,7 @@ class ParametersProcessor:
def check_template_parameter(self, parameter_name: str, def check_template_parameter(self, parameter_name: str,
parameter_value: Any, parameter_value: Any,
template_type: int, lineno: int) -> None: template_type: int, lineno: int) -> NoReturn:
'''Метод, проверяющий указанный параметр.''' '''Метод, проверяющий указанный параметр.'''
self.lineno = lineno self.lineno = lineno
self.template_type = template_type self.template_type = template_type
@ -293,7 +311,7 @@ class ParametersProcessor:
self._parameters_container.set_parameter({parameter_name: self._parameters_container.set_parameter({parameter_name:
checked_value}) checked_value})
def check_postparse_parameters(self) -> None: def check_postparse_parameters(self) -> NoReturn:
'''Метод, запускающий проверку параметров после их разбора.''' '''Метод, запускающий проверку параметров после их разбора.'''
for parameter, parameter_checker in\ for parameter, parameter_checker in\
self.postparse_checkers_list.items(): self.postparse_checkers_list.items():
@ -306,7 +324,7 @@ class ParametersProcessor:
result) result)
def check_template_parameters(self, parameters: dict, def check_template_parameters(self, parameters: dict,
template_type: int, lineno: int) -> None: template_type: int, lineno: int) -> NoReturn:
'''Метод, запускающий проверку указанных параметров.''' '''Метод, запускающий проверку указанных параметров.'''
self.template_type = template_type self.template_type = template_type
self.lineno = lineno self.lineno = lineno
@ -339,7 +357,6 @@ class ParametersProcessor:
parameter_name=checked_value) parameter_name=checked_value)
# Методы для проверки параметров во время разбора шаблона. # Методы для проверки параметров во время разбора шаблона.
def check_package_parameter(self, parameter_value: Any) -> str: def check_package_parameter(self, parameter_value: Any) -> str:
if not isinstance(parameter_value, str): if not isinstance(parameter_value, str):
raise IncorrectParameter("'package' parameter must have value of" raise IncorrectParameter("'package' parameter must have value of"
@ -402,17 +419,17 @@ class ParametersProcessor:
raise IncorrectParameter( raise IncorrectParameter(
"'restart' parameter value is not correct") "'restart' parameter value is not correct")
def check_stop_parameter(self, parameter_value: Any): def check_stop_parameter(self, parameter_value: Any) -> str:
if not parameter_value and isinstance(parameter_value, bool): if not parameter_value and isinstance(parameter_value, bool):
raise IncorrectParameter("'stop' parameter value is empty") raise IncorrectParameter("'stop' parameter value is empty")
return parameter_value return parameter_value
def check_start_parameter(self, parameter_value: Any): def check_start_parameter(self, parameter_value: Any) -> str:
if not parameter_value and isinstance(parameter_value, bool): if not parameter_value and isinstance(parameter_value, bool):
raise IncorrectParameter("'start' parameter value is empty") raise IncorrectParameter("'start' parameter value is empty")
return parameter_value return parameter_value
def check_run_parameter(self, parameter_value: Any): def check_run_parameter(self, parameter_value: Any) -> str:
if self.template_type == DIR: if self.template_type == DIR:
raise IncorrectParameter("'run' parameter is not available in" raise IncorrectParameter("'run' parameter is not available in"
" directory templates") " directory templates")
@ -425,7 +442,7 @@ class ParametersProcessor:
" found") " found")
return interpreter_path return interpreter_path
def check_exec_parameter(self, parameter_value: Any): def check_exec_parameter(self, parameter_value: Any) -> str:
if self.template_type == DIR: if self.template_type == DIR:
raise IncorrectParameter("'exec' parameter is not available in" raise IncorrectParameter("'exec' parameter is not available in"
" directory templates") " directory templates")
@ -438,7 +455,7 @@ class ParametersProcessor:
" found") " found")
return interpreter_path return interpreter_path
def check_chown_parameter(self, parameter_value: Any): def check_chown_parameter(self, parameter_value: Any) -> dict:
if not parameter_value or isinstance(parameter_value, bool): if not parameter_value or isinstance(parameter_value, bool):
raise IncorrectParameter("'chown' parameter value is empty.") raise IncorrectParameter("'chown' parameter value is empty.")
parameter_value = self.get_chown_values(parameter_value) parameter_value = self.get_chown_values(parameter_value)
@ -478,7 +495,8 @@ class ParametersProcessor:
x_mask = x_mask + "0" x_mask = x_mask + "0"
return (int(chmod, 2), int(x_mask, 2)) return (int(chmod, 2), int(x_mask, 2))
def check_source_parameter(self, parameter_value: Any): def check_source_parameter(self, parameter_value: Any
) -> Union[str, Tuple[bool, str]]:
if not parameter_value or isinstance(parameter_value, bool): if not parameter_value or isinstance(parameter_value, bool):
raise IncorrectParameter("'source' parameter value is empty") raise IncorrectParameter("'source' parameter value is empty")
@ -510,7 +528,7 @@ class ParametersProcessor:
return os.path.normpath(real_path) return os.path.normpath(real_path)
def check_env_parameter(self, parameter_value: Any): def check_env_parameter(self, parameter_value: Any) -> Union[None, set]:
env_set = set() env_set = set()
for env_value in parameter_value.split(','): for env_value in parameter_value.split(','):
@ -538,20 +556,20 @@ class ParametersProcessor:
return env_set return env_set
def check_force_parameter(self, parameter_value: Any): def check_force_parameter(self, parameter_value: Any) -> bool:
if isinstance(parameter_value, bool): if isinstance(parameter_value, bool):
return parameter_value return parameter_value
else: else:
raise IncorrectParameter("'force' parameter value is not bool") raise IncorrectParameter("'force' parameter value is not bool")
def check_autoupdate_parameter(self, parameter_value: Any): def check_autoupdate_parameter(self, parameter_value: Any) -> bool:
if isinstance(parameter_value, bool): if isinstance(parameter_value, bool):
return parameter_value return parameter_value
else: else:
raise IncorrectParameter( raise IncorrectParameter(
"'autoupdate' parameter value is not bool.") "'autoupdate' parameter value is not bool.")
def check_format_parameter(self, parameter_value: Any): def check_format_parameter(self, parameter_value: Any) -> str:
if self.template_type == DIR: if self.template_type == DIR:
raise IncorrectParameter("'format' parameter is redundant for" raise IncorrectParameter("'format' parameter is redundant for"
" directory templates.") " directory templates.")
@ -564,13 +582,13 @@ class ParametersProcessor:
raise IncorrectParameter("'format' parameter must be string value not" raise IncorrectParameter("'format' parameter must be string value not"
f" {type(parameter_value)}.") f" {type(parameter_value)}.")
def check_handler_parameter(self, parameter_value: Any): def check_handler_parameter(self, parameter_value: Any) -> str:
if not isinstance(parameter_value, str): if not isinstance(parameter_value, str):
raise IncorrectParameter("'handler' parameter must be string" raise IncorrectParameter("'handler' parameter must be string"
f" value not {type(parameter_value)}.") f" value not {type(parameter_value)}.")
return parameter_value return parameter_value
def check_notify_parameter(self, parameter_value: Any): def check_notify_parameter(self, parameter_value: Any) -> List[str]:
if isinstance(parameter_value, list): if isinstance(parameter_value, list):
return parameter_value return parameter_value
elif isinstance(parameter_value, str): elif isinstance(parameter_value, str):
@ -579,9 +597,33 @@ class ParametersProcessor:
raise IncorrectParameter("'notify' parameter must be string or list" raise IncorrectParameter("'notify' parameter must be string or list"
f" value not {type(parameter_value)}.") f" value not {type(parameter_value)}.")
# Методы для проверки параметров после разбора всего шаблона. def check_convert_parameter(self, parameter_value: Any) -> str:
if not isinstance(parameter_value, str):
raise IncorrectParameter("'convert' parameter value must be string"
f" not '{type(parameter_value)}'.")
parameter_value = parameter_value.strip().upper()
try:
available_image_formats =\
self.datavars_module.main.cl_image_formats
except VariableNotFoundError:
# TODO возможно стоит кидать ошибку.
available_image_formats = ["JPEG", "PNG", "GIF", "JPG"]
if parameter_value not in available_image_formats:
raise IncorrectParameter(f"'{parameter_value}' image format is "
"not available. Available image formats: "
f"'{', '.join(available_image_formats)}.'"
)
return parameter_value
def check_stretch_parameter(self, parameter_value: Any) -> bool:
if not isinstance(parameter_value, bool):
raise IncorrectParameter("'stretch' parameter value should be bool"
f" value not '{type(parameter_value)}'")
return parameter_value
def check_postparse_append(self, parameter_value): # Методы для проверки параметров после разбора всего шаблона.
def check_postparse_append(self, parameter_value: str) -> NoReturn:
if parameter_value == 'link': if parameter_value == 'link':
if 'source' not in self._parameters_container: if 'source' not in self._parameters_container:
raise IncorrectParameter("append = 'link' without source " raise IncorrectParameter("append = 'link' without source "
@ -595,7 +637,7 @@ class ParametersProcessor:
raise IncorrectParameter("'append' parameter is not 'compatible' " raise IncorrectParameter("'append' parameter is not 'compatible' "
"with the 'exec' parameter") "with the 'exec' parameter")
def check_postparse_run(self, parameter_value): def check_postparse_run(self, parameter_value: str) -> NoReturn:
if self._parameters_container.append: if self._parameters_container.append:
raise IncorrectParameter("'run' parameter is not 'compatible' " raise IncorrectParameter("'run' parameter is not 'compatible' "
"with the 'append' parameter") "with the 'append' parameter")
@ -604,7 +646,7 @@ class ParametersProcessor:
raise IncorrectParameter("'run' parameter is not 'compatible' " raise IncorrectParameter("'run' parameter is not 'compatible' "
"with the 'exec' parameter") "with the 'exec' parameter")
def check_postparse_exec(self, parameter_value): def check_postparse_exec(self, parameter_value: str) -> NoReturn:
if self._parameters_container.append: if self._parameters_container.append:
raise IncorrectParameter("'exec' parameter is not 'compatible' " raise IncorrectParameter("'exec' parameter is not 'compatible' "
"with the 'append' parameter") "with the 'append' parameter")
@ -613,13 +655,16 @@ class ParametersProcessor:
raise IncorrectParameter("'exec' parameter is not 'compatible' " raise IncorrectParameter("'exec' parameter is not 'compatible' "
"with the 'run' parameter") "with the 'run' parameter")
def check_postparse_source(self, parameter_value): def check_postparse_source(self,
parameter_value: Union[str, Tuple[bool, str]]
) -> NoReturn:
# Если файл по пути source не существует, но присутствует параметр # Если файл по пути source не существует, но присутствует параметр
# mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть # mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть
# удален в исполнительном модуле. # удален в исполнительном модуле.
if isinstance(parameter_value, tuple): if isinstance(parameter_value, tuple):
if (self._parameters_container.append == "link" and if ((self._parameters_container.append == "link" and
self._parameters_container.force): self._parameters_container.force)
or self._parameters_container.format == "backgrounds"):
self._parameters_container['source'] = parameter_value[1] self._parameters_container['source'] = parameter_value[1]
elif not self._parameters_container.mirror: elif not self._parameters_container.mirror:
raise IncorrectParameter( raise IncorrectParameter(
@ -632,12 +677,12 @@ class ParametersProcessor:
"append = 'link' for directory template") "append = 'link' for directory template")
) )
def check_postparse_autoupdate(self, parameter_value): def check_postparse_autoupdate(self, parameter_value: bool) -> NoReturn:
if self._parameters_container.unbound: if self._parameters_container.unbound:
raise IncorrectParameter("'unbound' parameter is incompatible" raise IncorrectParameter("'unbound' parameter is incompatible"
" with 'autoupdate' parameter") " with 'autoupdate' parameter")
def check_postparse_handler(self, parameter_value): def check_postparse_handler(self, parameter_value: bool) -> NoReturn:
if self._parameters_container.merge: if self._parameters_container.merge:
raise IncorrectParameter("'merge' parameter is not available" raise IncorrectParameter("'merge' parameter is not available"
" in handler templates") " in handler templates")
@ -646,7 +691,7 @@ class ParametersProcessor:
raise IncorrectParameter("'package' parameter is not available" raise IncorrectParameter("'package' parameter is not available"
" in handler templates") " in handler templates")
def check_postparse_package(self, parameter_value): def check_postparse_package(self, parameter_value: str) -> NoReturn:
groups = [] groups = []
package_atom = PackageAtomParser.parse_atom_name(parameter_value) package_atom = PackageAtomParser.parse_atom_name(parameter_value)
@ -679,7 +724,8 @@ class ParametersProcessor:
" does not match the template condition", " does not match the template condition",
self.lineno if hasattr(self, 'lineno') else 0) self.lineno if hasattr(self, 'lineno') else 0)
def _check_package_group(self, package: dict, group_packages: list): def _check_package_group(self, package: dict, group_packages: list
) -> bool:
'''Метод для проверки соответствия описания пакета, заданного словарем, '''Метод для проверки соответствия описания пакета, заданного словарем,
какому-либо описанию пакета, заданного в переменных groups.''' какому-либо описанию пакета, заданного в переменных groups.'''
for group_package in group_packages: for group_package in group_packages:
@ -699,16 +745,28 @@ class ParametersProcessor:
return True return True
return False return False
def check_postparse_convert(self, parameter_value: str) -> NoReturn:
template_format = self._parameters_container.format
if not template_format or template_format != "backgrounds":
raise IncorrectParameter("'convert' parameter available for"
" 'backgrounds' format only.")
def check_postparse_stretch(self, parameter_value: str) -> NoReturn:
template_format = self._parameters_container.format
if not template_format or template_format != "backgrounds":
raise IncorrectParameter("'stretch' parameter available for"
" 'backgrounds' format only.")
# Методы для проверки того, являются ли параметры наследуемыми. # Методы для проверки того, являются ли параметры наследуемыми.
def is_chmod_inheritable(self, parameter_value): def is_chmod_inheritable(self, parameter_value: str) -> bool:
chmod_regex = re.compile(r'\d+') chmod_regex = re.compile(r'\d+')
if chmod_regex.search(parameter_value): if chmod_regex.search(parameter_value):
return False return False
return True return True
def get_chown_values(self, chown: str): def get_chown_values(self, chown: str) -> dict:
"""Получить значения uid и gid из параметра chown.""" """Получить значения uid и gid из параметра chown."""
if chown and ':' in chown: if chown and ':' in chown:
user_name, group_name = chown.split(':') user_name, group_name = chown.split(':')
@ -746,7 +804,7 @@ class ParametersProcessor:
raise IncorrectParameter("'chown' value '{0}' is not correct". raise IncorrectParameter("'chown' value '{0}' is not correct".
format(chown)) format(chown))
def get_uid_from_passwd(self, user_name: str): def get_uid_from_passwd(self, user_name: str) -> int:
"""Функция для получения uid из chroot passwd файла.""" """Функция для получения uid из chroot passwd файла."""
passwd_file_path = os.path.join(self.chroot_path, 'etc/passwd') passwd_file_path = os.path.join(self.chroot_path, 'etc/passwd')
passwd_dictionary = dict() passwd_dictionary = dict()
@ -771,7 +829,7 @@ class ParametersProcessor:
raise FilesError("passwd file was not found in {}". raise FilesError("passwd file was not found in {}".
format(passwd_file_path)) format(passwd_file_path))
def get_gid_from_group(self, group_name: str): def get_gid_from_group(self, group_name: str) -> int:
"""Функция для получения gid из chroot group файла.""" """Функция для получения gid из chroot group файла."""
group_file_path = os.path.join(self.chroot_path, 'etc/group') group_file_path = os.path.join(self.chroot_path, 'etc/group')
group_dictionary = dict() group_dictionary = dict()
@ -797,7 +855,7 @@ class ParametersProcessor:
format(group_file_path)) format(group_file_path))
@classmethod @classmethod
def _inspect_formats_package(cls): def _inspect_formats_package(cls) -> NoReturn:
'''Метод для определения множества доступных форматов и '''Метод для определения множества доступных форматов и
предоставляемых ими параметров.''' предоставляемых ими параметров.'''
if cls.format_is_inspected: if cls.format_is_inspected:
@ -845,9 +903,13 @@ class ParametersProcessor:
cls.formats_inspected = True cls.formats_inspected = True
def resolve_or_missing(context, key, missing=missing, env={}): def resolve_or_missing(context: "CalculateContext",
key: str, missing=missing,
env: Optional[set] = None) -> Any:
'''Переопределение функции из для поиска значений переменных из jinja2. '''Переопределение функции из для поиска значений переменных из jinja2.
Ищет переменные в datavars.''' Ищет переменные в datavars.'''
if env is None:
env = {}
datavars = context.parent['__datavars__'] datavars = context.parent['__datavars__']
if key in context.vars: if key in context.vars:
@ -871,7 +933,7 @@ class CalculateContext(Context):
сохранять их.''' сохранять их.'''
_env_set = set() _env_set = set()
def resolve(self, key): def resolve(self, key: str) -> Any:
if self._legacy_resolve_mode: if self._legacy_resolve_mode:
rv = resolve_or_missing(self, key, rv = resolve_or_missing(self, key,
env=self._env_set) env=self._env_set)
@ -881,7 +943,7 @@ class CalculateContext(Context):
return self.environment.undefined(name=key) return self.environment.undefined(name=key)
return rv return rv
def resolve_or_missing(self, key): def resolve_or_missing(self, key: str) -> Any:
if self._legacy_resolve_mode: if self._legacy_resolve_mode:
rv = self.resolve(key) rv = self.resolve(key)
if isinstance(rv, Undefined): if isinstance(rv, Undefined):
@ -894,56 +956,56 @@ class CalculateContext(Context):
class ParametersContainer(MutableMapping): class ParametersContainer(MutableMapping):
'''Класс для хранения параметров, взятых из шаблона, и передачи '''Класс для хранения параметров, взятых из шаблона, и передачи
их шаблонизатору.''' их шаблонизатору.'''
def __init__(self, parameters_dictionary=None): def __init__(self, parameters_dictionary: Optional[dict] = None):
# Слой ненаследуемых параметров. # Слой ненаследуемых параметров.
self.__parameters = {} self.__parameters: dict = {}
# Слой наследуемых параметров. # Слой наследуемых параметров.
if parameters_dictionary is not None: if parameters_dictionary is not None:
self.__inheritable = parameters_dictionary self.__inheritable: dict = parameters_dictionary
else: else:
self.__inheritable = {} self.__inheritable: dict = {}
def set_parameter(self, item_to_add: dict): def set_parameter(self, item_to_add: dict) -> NoReturn:
self.__parameters.update(item_to_add) self.__parameters.update(item_to_add)
def set_inheritable(self, item_to_add: dict): def set_inheritable(self, item_to_add: dict) -> NoReturn:
self.__inheritable.update(item_to_add) self.__inheritable.update(item_to_add)
def get_inheritables(self): def get_inheritables(self) -> "ParametersContainer":
return ParametersContainer(copy.deepcopy(self.__inheritable)) return ParametersContainer(copy.deepcopy(self.__inheritable))
def remove_not_inheritable(self): def remove_not_inheritable(self) -> NoReturn:
self.__parameters.clear() self.__parameters.clear()
def print_parameters_for_debug(self): def print_parameters_for_debug(self) -> NoReturn:
print('Parameters:') print('Parameters:')
pprint(self.__parameters) pprint(self.__parameters)
print('Inherited:') print('Inherited:')
pprint(self.__inheritable) pprint(self.__inheritable)
def is_inherited(self, parameter_name): def is_inherited(self, parameter_name: str) -> bool:
return (parameter_name not in self.__parameters return (parameter_name not in self.__parameters
and parameter_name in self.__inheritable) and parameter_name in self.__inheritable)
def remove_parameter(self, parameter_name): def remove_parameter(self, parameter_name: str) -> NoReturn:
if parameter_name in self.__parameters: if parameter_name in self.__parameters:
self.__parameters.pop(parameter_name) self.__parameters.pop(parameter_name)
elif parameter_name in self.__inheritable: elif parameter_name in self.__inheritable:
self.__inheritable.pop(parameter_name) self.__inheritable.pop(parameter_name)
def change_parameter(self, parameter, value): def change_parameter(self, parameter: str, value: Any) -> NoReturn:
if parameter in self.__parameters: if parameter in self.__parameters:
self.__parameters.update({parameter: value}) self.__parameters.update({parameter: value})
elif parameter in self.__inheritable: elif parameter in self.__inheritable:
self.__inheritable.update({parameter: value}) self.__inheritable.update({parameter: value})
def _clear_container(self): def _clear_container(self) -> NoReturn:
self.__parameters.clear() self.__parameters.clear()
self.__inheritable.clear() self.__inheritable.clear()
def __getattr__(self, parameter_name): def __getattr__(self, parameter_name: str) -> Any:
if (parameter_name not in if (parameter_name not in
ParametersProcessor.available_parameters): ParametersProcessor.available_parameters):
raise IncorrectParameter("Unknown parameter: '{}'". raise IncorrectParameter("Unknown parameter: '{}'".
@ -956,7 +1018,7 @@ class ParametersContainer(MutableMapping):
else: else:
return False return False
def __getitem__(self, name): def __getitem__(self, name: str) -> Any:
if name in self.__parameters: if name in self.__parameters:
return self.__parameters[name] return self.__parameters[name]
elif name in self.__inheritable: elif name in self.__inheritable:
@ -964,31 +1026,31 @@ class ParametersContainer(MutableMapping):
else: else:
return False return False
def __setitem__(self, name, value): def __setitem__(self, name: str, value: Any) -> NoReturn:
self.__parameters[name] = value self.__parameters[name] = value
def __delitem__(self, name): def __delitem__(self, name: str) -> NoReturn:
if name in self.__parameters: if name in self.__parameters:
del self.__parameters[name] del self.__parameters[name]
if name in self.__inheritable: if name in self.__inheritable:
del self.__inheritable[name] del self.__inheritable[name]
def __iter__(self): def __iter__(self) -> Iterator[str]:
return iter(set(self.__parameters).union(self.__inheritable)) return iter(set(self.__parameters).union(self.__inheritable))
def __len__(self): def __len__(self) -> int:
return len(set(self.__parameters).union(self.__inheritable)) return len(set(self.__parameters).union(self.__inheritable))
def __repr__(self): def __repr__(self) -> str:
return '<ParametersContainer: parameters={0}, inheritables={1}>'.\ return '<ParametersContainer: parameters={0}, inheritables={1}>'.\
format(self.__parameters, self.__inheritable) format(self.__parameters, self.__inheritable)
def __contains__(self, name): def __contains__(self, name: str) -> bool:
return name in self.__parameters or name in self.__inheritable return name in self.__parameters or name in self.__inheritable
@property @property
def parameters(self): def parameters(self) -> dict:
return self.__parameters return self.__parameters
@ -999,10 +1061,14 @@ class CalculateExtension(Extension):
# Виды операций в теге save. # Виды операций в теге save.
ASSIGN, APPEND, REMOVE = range(3) ASSIGN, APPEND, REMOVE = range(3)
def __init__(self, environment, parameters_processor: ParametersProcessor, def __init__(self, environment: Environment,
datavars_module=Variables(), chroot_path="/"): parameters_processor: ParametersProcessor,
datavars_module: Union[Datavars,
NamespaceNode,
Variables] = Variables(),
chroot_path: str = "/"):
super().__init__(environment) super().__init__(environment)
self.environment = environment self.environment: Environment = environment
self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path) self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path)
self.environment.globals.update({'pkg': self.pkg}) self.environment.globals.update({'pkg': self.pkg})
@ -1011,11 +1077,11 @@ class CalculateExtension(Extension):
self._datavars = datavars_module self._datavars = datavars_module
self.parameters_processor = parameters_processor self.parameters_processor = parameters_processor
self.template_type = DIR self.template_type: int = DIR
# Флаг, указывающий, что тег calculate уже был разобран. Нужен для # Флаг, указывающий, что тег calculate уже был разобран. Нужен для
# того, чтобы проверять единственность тега calculate. # того, чтобы проверять единственность тега calculate.
self.calculate_parsed = False self.calculate_parsed: bool = False
self.tags = {'calculate', 'save', 'set_var'} self.tags = {'calculate', 'save', 'set_var'}
self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'} self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'}
@ -1031,24 +1097,25 @@ class CalculateExtension(Extension):
self.parse_methods = {'calculate': self.parse_calculate, self.parse_methods = {'calculate': self.parse_calculate,
'save': self.parse_save} 'save': self.parse_save}
def __call__(self, env): def __call__(self, env: Environment) -> "CalculateExtension":
# Необходимо для обеспечения возможности передать готовый объект # Необходимо для обеспечения возможности передать готовый объект
# расширения, а не его класс. # расширения, а не его класс.
return self return self
def parse(self, parser): def parse(self, parser: Parser) -> List[nodes.Output]:
self.parser = parser self.parser = parser
self.stream = parser.stream self.stream = parser.stream
tag_token = self.stream.current.value tag_token = self.stream.current.value
return [self.parse_methods[tag_token]()] return [self.parse_methods[tag_token]()]
def parse_save(self): def parse_save(self) -> nodes.Output:
'''Метод для разбора тега save, сохраняющего значение указанной '''Метод для разбора тега save, сохраняющего значение указанной
переменной datavars.''' переменной datavars.'''
lineno = next(self.stream).lineno lineno = next(self.stream).lineno
target_file = nodes.Const('', lineno=lineno) target_file = nodes.Const('', lineno=lineno)
# Получаем имя целевого файла.
if self.stream.skip_if('dot'): if self.stream.skip_if('dot'):
target_file_name = self.stream.expect('name').value target_file_name = self.stream.expect('name').value
if target_file_name in self.TARGET_FILES_SET: if target_file_name in self.TARGET_FILES_SET:
@ -1084,7 +1151,7 @@ class CalculateExtension(Extension):
raise TemplateSyntaxError("'=' is expected in 'save' tag", raise TemplateSyntaxError("'=' is expected in 'save' tag",
lineno=lineno) lineno=lineno)
def parse_calculate(self): def parse_calculate(self) -> nodes.Output:
'''Метод для разбора тега calculate, содержащего значения параметров и '''Метод для разбора тега calculate, содержащего значения параметров и
условия выполнения шаблона.''' условия выполнения шаблона.'''
lineno = next(self.stream).lineno lineno = next(self.stream).lineno
@ -1155,7 +1222,7 @@ class CalculateExtension(Extension):
self.calculate_parsed = True self.calculate_parsed = True
return nodes.Output([nodes.Const('')], lineno=lineno) return nodes.Output([nodes.Const('')], lineno=lineno)
def _is_variable_name(self, token): def _is_variable_name(self, token: Token) -> bool:
'''Метод для проверки токена на предмет того, что он является частью '''Метод для проверки токена на предмет того, что он является частью
имени переменной.''' имени переменной.'''
if not token.type == 'name': if not token.type == 'name':
@ -1168,7 +1235,8 @@ class CalculateExtension(Extension):
return True return True
return False return False
def check_parameter(self, parameter_name, parameter_value, context): def check_parameter(self, parameter_name: str, parameter_value: Any,
context: CalculateContext) -> str:
self.parameters_processor.check_template_parameter( self.parameters_processor.check_template_parameter(
parameter_name, parameter_name,
parameter_value, parameter_value,
@ -1176,7 +1244,7 @@ class CalculateExtension(Extension):
self.stream.current.lineno) self.stream.current.lineno)
return '' return ''
def parse_condition(self): def parse_condition(self) -> Template:
try: try:
condition_node = self.parser.parse_expression(with_condexpr=True) condition_node = self.parser.parse_expression(with_condexpr=True)
condition_node = self.call_method( condition_node = self.call_method(
@ -1196,7 +1264,7 @@ class CalculateExtension(Extension):
.format(str(error)), .format(str(error)),
lineno=self.stream.current.lineno) lineno=self.stream.current.lineno)
def check_conditions(self, conditions: list): def check_conditions(self, conditions: List[Template]) -> NoReturn:
for condition in conditions: for condition in conditions:
self.condition_result = False self.condition_result = False
try: try:
@ -1210,7 +1278,7 @@ class CalculateExtension(Extension):
lineno=self.stream.current.lineno) lineno=self.stream.current.lineno)
# DEPRECATED # DEPRECATED
def get_condition_result(self): def get_condition_result(self) -> bool:
'''Метод для разбора условий из тега calculate.''' '''Метод для разбора условий из тега calculate.'''
self.condition_result = False self.condition_result = False
@ -1233,13 +1301,14 @@ class CalculateExtension(Extension):
return self.condition_result return self.condition_result
def set_condition_result(self, condition_result): def set_condition_result(self, condition_result: Any) -> str:
'''Метод для сохранения результата вычисления условия.''' '''Метод для сохранения результата вычисления условия.'''
self.condition_result = condition_result self.condition_result = condition_result
return '' return ''
def _make_save_node(self, variable_name_node, target_file_node, optype, def _make_save_node(self, variable_name_node: nodes.List,
lineno): target_file_node: nodes.Const, optype: int,
lineno: int) -> nodes.Output:
'''Метод для создания ноды, сохраняющей переменные.''' '''Метод для создания ноды, сохраняющей переменные.'''
right_value = self.parser.parse_expression(with_condexpr=True) right_value = self.parser.parse_expression(with_condexpr=True)
optype_node = nodes.Const(optype, lineno=lineno) optype_node = nodes.Const(optype, lineno=lineno)
@ -1252,8 +1321,9 @@ class CalculateExtension(Extension):
lineno=lineno) lineno=lineno)
return nodes.Output([save_variable_node], lineno=lineno) return nodes.Output([save_variable_node], lineno=lineno)
def save_variable(self, variable, right_value, target_file, def save_variable(self, variable: List[str], right_value: Any,
optype, context): target_file: str, optype: int,
context: CalculateContext) -> str:
'''Метод для сохранения значений переменных указанных в теге save.''' '''Метод для сохранения значений переменных указанных в теге save.'''
datavars = context.parent['__datavars__'] datavars = context.parent['__datavars__']
if variable[0] not in datavars: if variable[0] not in datavars:
@ -1272,7 +1342,7 @@ class CalculateExtension(Extension):
# Теперь меняем знaчение переменной. # Теперь меняем знaчение переменной.
if isinstance(value_container, NamespaceNode): if isinstance(value_container, NamespaceNode):
self._modify_variables(variable, value_container, right_value, self._modify_variables(variable, value_container, right_value,
optype, target=target_file, optype, target_file=target_file,
modify_only=modify_only) modify_only=modify_only)
elif isinstance(value_container, VariableNode): elif isinstance(value_container, VariableNode):
hash_value = value_container.get_value().get_hash() hash_value = value_container.get_value().get_hash()
@ -1296,7 +1366,7 @@ class CalculateExtension(Extension):
def _find_value_container(self, variable: List[str], def _find_value_container(self, variable: List[str],
vars_package: NamespaceNode, vars_package: NamespaceNode,
modify_only: bool = True modify_only: bool = True
) -> Union[NamespaceNode]: ) -> Union[NamespaceNode, VariableNode]:
'''Метод для поиска контейнера, путь к которому указан в аргументе. '''Метод для поиска контейнера, путь к которому указан в аргументе.
Этим контейнером может быть пространство имен или хэш.''' Этим контейнером может быть пространство имен или хэш.'''
current_container = vars_package current_container = vars_package
@ -1329,8 +1399,10 @@ class CalculateExtension(Extension):
current_container.get_fullname())) current_container.get_fullname()))
return current_container return current_container
def _modify_variables(self, variable, namespace, new_value, optype, def _modify_variables(self, variable: List[str], namespace: NamespaceNode,
target='', modify_only=True): new_value: Any, optype: int,
target_file: Optional[str] = None,
modify_only: bool = True) -> NoReturn:
'''Метод для модификации значения переменной.''' '''Метод для модификации значения переменной.'''
variable_name = variable[-1] variable_name = variable[-1]
@ -1353,18 +1425,20 @@ class CalculateExtension(Extension):
raise SaveError("can not create variable '{}' in not 'custom'" raise SaveError("can not create variable '{}' in not 'custom'"
" namespace".format('.'.join(variable))) " namespace".format('.'.join(variable)))
if target: if target_file:
if namespace._variables[variable_name].variable_type is HashType: if namespace._variables[variable_name].variable_type is HashType:
for key, value in new_value.items(): for key, value in new_value.items():
self._save_to_target(variable, key, value, target) self._save_to_target(variable, key, value, target_file)
else: else:
self._save_to_target(variable[:-1], variable_name, self._save_to_target(variable[:-1], variable_name,
new_value, target) new_value, target_file)
def _modify_hash(self, variable, hash_variable, new_value, optype, # DEPRECATED
target=''): def _modify_hash(self, variable_name: List[str],
hash_variable: VariableNode, new_value, optype,
target_file: Optional[str] = None) -> NoReturn:
'''Метод для модификации значения в переменной-хэше.''' '''Метод для модификации значения в переменной-хэше.'''
value_name = variable[-1] value_name = variable_name[-1]
hash_value = hash_variable.get_value().get_hash() hash_value = hash_variable.get_value().get_hash()
if value_name in hash_value: if value_name in hash_value:
@ -1376,22 +1450,25 @@ class CalculateExtension(Extension):
hash_value.update({value_name: new_value}) hash_value.update({value_name: new_value})
hash_variable.set(hash_value) hash_variable.set(hash_value)
if target: if target_file:
self._save_to_target(variable[:-1], value_name, self._save_to_target(variable_name[:-1], value_name,
new_value, target) new_value, target_file)
def _save_to_target(self, namespace_name, variable_name, value, target): def _save_to_target(self, namespace_name: List[str],
variable_name: str, value: Any, target_file: str
) -> NoReturn:
'''Метод для добавления переменной в список переменных, значение '''Метод для добавления переменной в список переменных, значение
которых было установлено через тег save и при этом должно быть которых было установлено через тег save и при этом должно быть
сохранено в указанном файле: save.target_file.''' сохранено в указанном файле: save.target_file.'''
namespace_name = tuple(namespace_name) namespace_name = tuple(namespace_name)
target_file_dict = self._datavars.variables_to_save[target] target_file_dict = self._datavars.variables_to_save[target_file]
if namespace_name not in target_file_dict: if namespace_name not in target_file_dict:
target_file_dict.update({namespace_name: dict()}) target_file_dict.update({namespace_name: dict()})
target_file_dict[namespace_name].update( target_file_dict[namespace_name].update(
{variable_name: ('=', str(value))}) {variable_name: ('=', str(value))})
def _append_variable_value(self, variable, value): def _append_variable_value(self, variable: VariableNode,
value: Any) -> Any:
'''Метод описывающий операцию += в теге save.''' '''Метод описывающий операцию += в теге save.'''
variable_value = variable.get_value() variable_value = variable.get_value()
if (variable.variable_type is IntegerType or if (variable.variable_type is IntegerType or
@ -1429,7 +1506,8 @@ class CalculateExtension(Extension):
# значение. # значение.
return variable_value return variable_value
def _remove_variable_value(self, variable, value): def _remove_variable_value(self, variable: VariableNode, value: Any
) -> Any:
'''Метод описывающий операцию -= в теге save.''' '''Метод описывающий операцию -= в теге save.'''
variable_value = variable.get_value() variable_value = variable.get_value()
@ -1469,7 +1547,7 @@ class CalculateExtension(Extension):
# значение. # значение.
return variable_value return variable_value
def _get_parameter(self): def _get_parameter(self) -> Tuple[nodes.Const, nodes.Node]:
'''Метод для разбора параметров, содержащихся в теге calculate.''' '''Метод для разбора параметров, содержащихся в теге calculate.'''
lineno = self.stream.current.lineno lineno = self.stream.current.lineno
@ -1491,13 +1569,14 @@ class CalculateExtension(Extension):
return (parameter_name_node, parameter_rvalue) return (parameter_name_node, parameter_rvalue)
def save_parameters(cls, parameters_dictionary, context): def save_parameters(cls, parameters_dictionary: dict,
context: CalculateContext) -> str:
'''Метод для сохранения значений параметров.''' '''Метод для сохранения значений параметров.'''
context.parent['__parameters__'].set_parameter(parameters_dictionary) context.parent['__parameters__'].set_parameter(parameters_dictionary)
return '' return ''
@contextfunction @contextfunction
def pkg(self, context, *args) -> Version: def pkg(self, context: CalculateContext, *args: dict) -> Version:
'''Метод, реализующий функцию pkg() шаблонов. Функция предназначена для '''Метод, реализующий функцию pkg() шаблонов. Функция предназначена для
получения версии пакета, к которому уже привязан шаблон, если получения версии пакета, к которому уже привязан шаблон, если
аргументов нет, или версию пакета в аргументе функции. Если аргументов аргументов нет, или версию пакета в аргументе функции. Если аргументов
@ -1518,7 +1597,7 @@ class CalculateExtension(Extension):
return Version() return Version()
return package.version return package.version
def get_full_filepath(self, fname): def get_full_filepath(self, fname: str) -> str:
# TODO: добавить получение домашней директории пользователя # TODO: добавить получение домашней директории пользователя
# if fname[0] == "~": # if fname[0] == "~":
# # Получаем директорию пользователя # # Получаем директорию пользователя
@ -1531,10 +1610,9 @@ class CalculateExtension(Extension):
return fname return fname
@contextfunction @contextfunction
def grep(self, context, fname, regpattern) -> str: def grep(self, context: CalculateContext, fname: str,
''' regpattern: str) -> str:
Метод реализующий функцию grep '''Метод реализующий функцию grep.'''
'''
fname = self.get_full_filepath(fname) fname = self.get_full_filepath(fname)
try: try:
reg = re.compile(regpattern, re.MULTILINE) reg = re.compile(regpattern, re.MULTILINE)
@ -1554,10 +1632,8 @@ class CalculateExtension(Extension):
return "" return ""
@contextfunction @contextfunction
def exists(self, context, fname) -> str: def exists(self, context: CalculateContext, fname: str) -> str:
''' '''Метод реализующий функцию exists.'''
Метод реализующий функцию exists
'''
fname = self.get_full_filepath(fname) fname = self.get_full_filepath(fname)
try: try:
@ -1627,19 +1703,20 @@ class TemplateEngine:
self.environment.context_class = CalculateContext self.environment.context_class = CalculateContext
@property @property
def for_package(self): def for_package(self) -> Package:
return self.parameters_processor.for_package return self.parameters_processor.for_package
@for_package.setter @for_package.setter
def for_package(self, package): def for_package(self, package: Package) -> NoReturn:
self.parameters_processor.for_package = package self.parameters_processor.for_package = package
def change_directory(self, directory_path): def change_directory(self, directory_path: str) -> NoReturn:
'''Метод для смены директории в загрузчике.''' '''Метод для смены директории в загрузчике.'''
self.environment.loader = FileSystemLoader(directory_path) self.environment.loader = FileSystemLoader(directory_path)
def process_template(self, template_path, template_type, def process_template(self, template_path: str, template_type: str,
parameters=None): parameters: Optional[ParametersContainer] = None
) -> NoReturn:
'''Метод для обработки файла шаблона, расположенного по указанному '''Метод для обработки файла шаблона, расположенного по указанному
пути.''' пути.'''
if parameters is not None: if parameters is not None:
@ -1666,8 +1743,10 @@ class TemplateEngine:
Version=Version Version=Version
) )
def process_template_from_string(self, string, template_type, def process_template_from_string(
parameters=None): self, string: str, template_type: int,
parameters: Optional[ParametersContainer] = None
) -> NoReturn:
'''Метод для обработки текста шаблона.''' '''Метод для обработки текста шаблона.'''
if parameters is not None: if parameters is not None:
self._parameters_object = parameters self._parameters_object = parameters
@ -1694,10 +1773,10 @@ class TemplateEngine:
) )
@property @property
def parameters(self): def parameters(self) -> ParametersContainer:
return self._parameters_object return self._parameters_object
@property @property
def template_text(self): def template_text(self) -> str:
text, self._template_text = self._template_text, '' text, self._template_text = self._template_text, ''
return text return text

@ -275,7 +275,8 @@ class TemplateWrapper:
# Если установлен параметр mirror и есть параметр source, # Если установлен параметр mirror и есть параметр source,
# содержащий несуществующий путь -- удаляем целевой файл. # содержащий несуществующий путь -- удаляем целевой файл.
if self.parameters.source is True and self.parameters.mirror: if (self.parameters.source is True and self.parameters.mirror and
not self.format_class.EXECUTABLE):
self.remove_original = True self.remove_original = True
else: else:
self.target_type = None self.target_type = None
@ -283,13 +284,10 @@ class TemplateWrapper:
if self.format_class is not None and self.format_class.EXECUTABLE: if self.format_class is not None and self.format_class.EXECUTABLE:
# Если формат исполняемый -- проверяем, существует ли директория, # Если формат исполняемый -- проверяем, существует ли директория,
# из которой будет выполняться шаблон. # из которой будет выполняться шаблон.
if not os.path.exists(self.target_path): if self.target_type is None:
# Если не существует -- создаем ее. # Если не существует -- создаем ее.
os.makedirs(self.target_path) if not os.path.exists(os.path.dirname(self.target_path)):
elif os.path.isfile(self.target_path): os.makedirs(os.path.dirname(self.target_path))
# Если вместо директории файл -- определяем по файлу
# директорию.
self.target_path = os.path.dirname(self.target_path)
# Если есть параметр package, определяем по нему пакет. # Если есть параметр package, определяем по нему пакет.
if self.parameters.package: if self.parameters.package:
@ -656,11 +654,12 @@ class TemplateWrapper:
def update_contents_from_list(self, changed_list: dict) -> NoReturn: def update_contents_from_list(self, changed_list: dict) -> NoReturn:
'''Метод для изменения CONTENTS по списку измененных файлов.''' '''Метод для изменения CONTENTS по списку измененных файлов.'''
print("UPDATE CONTENTS FROM LIST")
if self.target_package is None: if self.target_package is None:
return return
for file_path, mode in changed_list.items(): for file_path, mode in changed_list.items():
if mode == "M": if mode in {"M", "N"}:
if os.path.islink(file_path): if os.path.islink(file_path):
self.target_package.add_sym(file_path) self.target_package.add_sym(file_path)
@ -790,7 +789,7 @@ class TemplateExecutor:
self.executor_output = {'target_path': None, self.executor_output = {'target_path': None,
'stdout': None, 'stdout': None,
'stderr': None, 'stderr': None,
'warning': None} 'warnings': []}
if parameters.append == 'skip': if parameters.append == 'skip':
return self.executor_output return self.executor_output
@ -859,7 +858,7 @@ class TemplateExecutor:
template_object.target_path template_object.target_path
if template_object.ca_is_missed: if template_object.ca_is_missed:
self.executor_output['warning'] = ( self.executor_output['warnings'].append(
"archive file is missed," "archive file is missed,"
" target file was used instead") " target file was used instead")
@ -1027,10 +1026,8 @@ class TemplateExecutor:
# Действия, если целевой файл не имеет пользовательских изменений # Действия, если целевой файл не имеет пользовательских изменений
# или если он исполнительный. # или если он исполнительный.
# Парсим текст шаблона используя его формат. # Парсим текст шаблона используя его формат.
parsed_template = template_format(template_object.template_text, if (not template_object.template_text.strip()
template_object.template_path, and template_object.parameters.source
ignore_comments=True)
if (not parsed_template and template_object.parameters.source
and template_object.parameters.append == "replace" and template_object.parameters.append == "replace"
and template_object.parameters.format == "raw"): and template_object.parameters.format == "raw"):
# Если шаблон пуст, параметром source задан входной файл, # Если шаблон пуст, параметром source задан входной файл,
@ -1042,6 +1039,10 @@ class TemplateExecutor:
chown=chown, chown=chown,
chmod=chmod) chmod=chmod)
elif not template_object.format_class.EXECUTABLE: elif not template_object.format_class.EXECUTABLE:
parsed_template = template_format(
template_object.template_text,
template_object.template_path,
ignore_comments=True)
# Действия для шаблонов не являющихся исполнительными. # Действия для шаблонов не являющихся исполнительными.
output_paths = [output_path] output_paths = [output_path]
@ -1076,7 +1077,7 @@ class TemplateExecutor:
output_file_md5 = hashlib.md5(output_text.encode()).hexdigest() output_file_md5 = hashlib.md5(output_text.encode()).hexdigest()
if input_text: if input_text:
input_file_md5 = hashlib.md5( input_file_md5 = hashlib.md5(
input_text.encode()).hexdigest() input_text.encode()).hexdigest()
for save_path in output_paths: for save_path in output_paths:
if not os.path.exists(os.path.dirname(save_path)): if not os.path.exists(os.path.dirname(save_path)):
@ -1090,10 +1091,14 @@ class TemplateExecutor:
# если это необходимо. # если это необходимо.
if chown: if chown:
self._chown_file(save_path, chown) self._chown_file(save_path, chown)
if chmod: if chmod:
self._chmod_file(save_path, chmod) self._chmod_file(save_path, chmod)
elif template_object.format_class.EXECUTABLE: elif template_object.format_class.EXECUTABLE:
parsed_template = template_format(
template_object.template_text,
template_object.template_path,
template_object.parameters,
self.datavars_module)
changed_files = parsed_template.execute_format( changed_files = parsed_template.execute_format(
template_object.target_path, template_object.target_path,
chroot_path=self.chroot_path) chroot_path=self.chroot_path)
@ -1102,7 +1107,6 @@ class TemplateExecutor:
# Если исполняемый формат выдал список измененных файлов для # Если исполняемый формат выдал список измененных файлов для
# изменения CONTENTS и при этом задан пакет -- обновляем # изменения CONTENTS и при этом задан пакет -- обновляем
# CONTENTS. # CONTENTS.
for file_path, status in changed_files.items(): for file_path, status in changed_files.items():
if status == 'M': if status == 'M':
if file_path in self.changed_files: if file_path in self.changed_files:
@ -1119,6 +1123,12 @@ class TemplateExecutor:
else: else:
self.changed_files[file_path] = 'D' self.changed_files[file_path] = 'D'
# Если в ходе работы формат выкидывал предупреждения --
# добавляем их в список предупреждений.
if parsed_template.warnings:
self.executor_output['warnings'].extend(
parsed_template.warnings)
if (self.dbpkg and changed_files and if (self.dbpkg and changed_files and
template_object.target_package and template_object.target_package and
template_object.format_class.FORMAT != 'contents'): template_object.format_class.FORMAT != 'contents'):
@ -2853,8 +2863,8 @@ class DirectoryProcessor:
parameters: ParametersContainer) -> str: parameters: ParametersContainer) -> str:
'''Метод для получения пути к целевому файлу с учетом наличия '''Метод для получения пути к целевому файлу с учетом наличия
параметров name, path и append = skip.''' параметров name, path и append = skip.'''
# Если есть параметр name -- меняем имя шаблона. # Если есть непустой параметр name -- меняем имя шаблона.
if parameters.name: if parameters.name is not False and parameters.name != '':
template_name = parameters.name template_name = parameters.name
# Если для шаблона задан путь -- меняем путь к директории шаблона. # Если для шаблона задан путь -- меняем путь к директории шаблона.
@ -2924,9 +2934,12 @@ class DirectoryProcessor:
if output['target_path'] is not None: if output['target_path'] is not None:
target_path = output['target_path'] target_path = output['target_path']
if output['warning'] is not None: if output['warnings']:
self.output.set_warning(f"{output['warning']}." if not isinstance(output['warnings'], list):
f" Template: {template_path}") output['warnings'] = [output['warnings']]
for warning in output['warnings']:
self.output.set_warning(f"{warning}."
f" Template: {template_path}")
# Если есть вывод от параметра run -- выводим как info. # Если есть вывод от параметра run -- выводим как info.
if output['stdout'] is not None: if output['stdout'] is not None:

@ -1,6 +1,7 @@
# vim: fileencoding=utf-8 # vim: fileencoding=utf-8
# #
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from typing import Union, List
from io import TextIOWrapper from io import TextIOWrapper
from os import path from os import path
from .tools import GenericFS, get_traceback_caller from .tools import GenericFS, get_traceback_caller
@ -248,9 +249,9 @@ class ProgramPathCache:
'''Класс, для поиска и кэширования путей к исполнительным файлам различных '''Класс, для поиска и кэширования путей к исполнительным файлам различных
команд.''' команд.'''
def __init__(self): def __init__(self):
self._cache = {} self._cache: dict = {}
def __call__(self, program_name, prefix='/'): def __call__(self, program_name: str, prefix: str = '/'):
program_base_name = path.basename(program_name) program_base_name = path.basename(program_name)
PATH = os.environ['PATH'] PATH = os.environ['PATH']
PATH = PATH.split(':') PATH = PATH.split(':')
@ -274,7 +275,7 @@ class ProgramPathCache:
get_program_path = ProgramPathCache() get_program_path = ProgramPathCache()
def check_command(*utils): def check_command(*utils: List[str]):
'''Функция для проверки наличия той или иной команды системе.''' '''Функция для проверки наличия той или иной команды системе.'''
output = [] output = []
for util in utils: for util in utils:
@ -289,7 +290,7 @@ def check_command(*utils):
return output return output
def join_paths(*paths): def join_paths(*paths: List[str]) -> str:
'''Функция для объединения путей. Объединяет также абсолютные пути.''' '''Функция для объединения путей. Объединяет также абсолютные пути.'''
if len(paths) == 1: if len(paths) == 1:
return next(iter(paths)) return next(iter(paths))
@ -307,7 +308,7 @@ def join_paths(*paths):
return output_path return output_path
def read_link(file_path): def read_link(file_path: str) -> str:
'''Функция для получения целевого пути символьной ссылки.''' '''Функция для получения целевого пути символьной ссылки.'''
try: try:
if path.exists(file_path): if path.exists(file_path):
@ -321,7 +322,8 @@ def read_link(file_path):
format(str(error), mod, lineno)) format(str(error), mod, lineno))
def get_target_from_link(link_path, link_source, chroot_path='/'): def get_target_from_link(link_path: str, link_source: str,
chroot_path: str = '/') -> str:
'''Метод для получения целевого пути из целевого пути символьной ссылки '''Метод для получения целевого пути из целевого пути символьной ссылки
с учетом того, что целевой путь символьной ссылки может быть с учетом того, что целевой путь символьной ссылки может быть
относительным.''' относительным.'''
@ -342,11 +344,11 @@ def get_target_from_link(link_path, link_source, chroot_path='/'):
return '/'.join(link_dir) return '/'.join(link_dir)
def read_file(file_path): def read_file(file_path: str, binary: bool = False) -> Union[str, bytes]:
'''Функция для чтения файлов, возвращает текст файла.''' '''Функция для чтения файлов, возвращает текст файла.'''
try: try:
if path.exists(file_path): if path.exists(file_path):
with open(file_path, 'r') as opened_file: with open(file_path, f'r{"b" if binary else ""}') as opened_file:
return opened_file.read() return opened_file.read()
except (OSError, IOError) as error: except (OSError, IOError) as error:
mod, lineno = get_traceback_caller(*sys.exc_info()) mod, lineno = get_traceback_caller(*sys.exc_info())

@ -1,92 +1,135 @@
import os import os
import hashlib # import hashlib
from calculate.utils.files import Process, write_file, read_file from calculate.utils.files import Process, join_paths
from calculate.templates.format.base_format import FormatError
from typing import NoReturn, Union, List, Tuple, Optional
class ImageMagickError(Exception):
class ImageMagickError(FormatError):
pass pass
class ImageMagick: class ImageMagick:
def __init__(self, prefix='/'): def __init__(self, chroot: str = '/'):
self.prefix = prefix self._chroot_path: str = chroot
self.init_commands(prefix) self.init_commands(chroot)
self.default_opts = [] self.default_opts: List[str] = []
@property @property
def available(self): def available(self) -> bool:
return self.convert_cmd and self.identify_cmd return bool(self.convert_cmd and self.identify_cmd)
@property @property
def chroot(self): def chrooted(self) -> bool:
return self.prefix != '/' return self._chroot_path != '/'
def init_commands(self, prefix): def init_commands(self, chroot: str) -> NoReturn:
self.convert_cmd = "/usr/bin/convert" self.convert_cmd: str = "/usr/bin/convert"
self.identify_cmd = "/usr/bin/identify" self.identify_cmd: str = "/usr/bin/identify"
self.chroot_cmd = "/bin/chroot" self.chroot_cmd: str = "/bin/chroot"
self.bash_cmd = "/bin/bash" self.bash_cmd: str = "/bin/bash"
if not os.path.exists(os.path.join(prefix, self.convert_cmd[1:])):
if not os.path.exists(join_paths(chroot, self.convert_cmd)):
self.convert_cmd = None self.convert_cmd = None
if not os.path.exists(os.path.join(prefix, self.identify_cmd[1:])): if not os.path.exists(join_paths(chroot, self.identify_cmd)):
self.identify_cmd = None self.identify_cmd = None
def trim_prefix_path(self, filename): def trim_prefix_path(self, filename: str) -> Union[str, None]:
retpath = "/%s" % os.path.relpath(filename, self.prefix) retpath = "/{}".format(os.path.relpath(filename, self._chroot_path))
if retpath.startswith("/.."): if retpath.startswith("/.."):
return None return None
return retpath return retpath
def get_image_resolution(self, source): def get_image_resolution(self, source: str
if self.chroot: ) -> Union[None, Tuple[int, int]]:
identify = Process(self.chroot_cmd, self.prefix, '''Метод для получения разрешения указанного изображения,
с помощью команды 'identify -format %w %h <path_to_image>'.'''
print(f"SOURCE: {source}")
if self.chrooted:
print(f"CHROOT_PATH: {self._chroot_path}")
identify = Process(self.chroot_cmd, self._chroot_path,
self.bash_cmd, "-c", self.bash_cmd, "-c",
" ".join([self.identify_cmd, " ".join([self.identify_cmd,
"-format '%w %h'", source])) "-format '%w %h'", source]))
else: else:
identify = Process(self.identify_cmd, "-format", "%w %h", source) identify = Process(self.identify_cmd, "-format", "%w %h", source)
if identify.success(): if identify.success():
swidth, _sep, sheight = identify.read().strip().partition(" ") result = identify.read()
swidth, sheight = result.split(" ")
if swidth.isdigit() and sheight.isdigit(): if swidth.isdigit() and sheight.isdigit():
return int(swidth), int(sheight) return int(swidth), int(sheight)
return None else:
raise ImageMagickError(f"ERROR: can not parse: '{result}'")
else:
raise ImageMagickError(f"ERROR: {identify.read_error()}")
def convert(self, source, target, *opts): def convert(self, source: str, target: str, *opts: List[str],
image_format: Optional[str] = None) -> bool:
command = [self.convert_cmd, "-quality", "95", command = [self.convert_cmd, "-quality", "95",
source] source]
command.extend(self.default_opts) command.extend(self.default_opts)
command.extend(opts) command.extend(opts)
command.append(target) if image_format is not None:
if self.chroot: command.append(f"{image_format}:{target}")
convert = Process(self.chroot_cmd, self.prefix, else:
command.append(target)
if self.chrooted:
convert = Process(self.chroot_cmd, self._chroot_path,
self.bash_cmd, "-c", self.bash_cmd, "-c",
" ".join(command)) " ".join(command))
else: else:
print("OPTIONS:")
print(command)
convert = Process(*command) convert = Process(*command)
if convert.success(): if convert.success():
print("CREATED: {}".format(target))
return True return True
else: else:
print(convert.read_error()) print("ERROR:", convert.read_error())
return False return False
def convert_resize_crop_center(self, source, target, height, width): def convert_resize_crop_center(self, source: str, target: str,
#if ((width == self.source_width and height == self.source_height) and height: int, width: int,
# (source.rpartition('.')[2] == target.rpartition('.')[2])): image_format: Optional[str] = None) -> bool:
# with write_file(target) as sf: # if ((width == self.source_width and height == self.source_height) and
# sf.write(read_file(source)) # (source.rpartition('.')[2] == target.rpartition('.')[2])):
# return True # with write_file(target) as sf:
res = "%dx%d" % (width, height) # sf.write(read_file(source))
# return True
res = f"{width}x{height}"
return self.convert(source, target, "-quality", "95",
"-resize", f"{res}^",
"-strip", "-gravity", "center",
"-crop", f"{res}+0+0",
image_format=image_format)
def convert_resize_gfxboot(self, source: str, target: str,
height: int, width: int,
image_format: Optional[str] = None) -> bool:
res = f"{width}x{height}"
return self.convert(source, target, "-quality", "95", return self.convert(source, target, "-quality", "95",
"-resize", "%s^" % res, "-resize", f"{res}^",
"-strip", "-gravity", "center", "-strip", "-gravity", "center",
"-crop", "%s+0+0" % res) "-crop", f"{res}+0+0",
"-sampling-factor", "2x2",
"-interlace", "none",
"-set", "units", "PixelsPerSecond",
image_format=image_format)
def convert_resize_gfxboot(self, source, target, height, width): def get_image_format(self, source: str) -> str:
res = "%dx%d" % (width, height) """Метод для получения формата указанного файла."""
if self.chrooted:
identify = Process(self.chroot_cmd, self._chroot_path,
self.bash_cmd, "-c",
" ".join([self.identify_cmd,
"-format '%m'", source]))
else:
identify = Process(self.identify_cmd, "-format", "%m", source)
return self.convert(source, target, "-quality", "95", if identify.success():
"-resize", "%s^" % res, image_format = identify.read()
"-strip", "-gravity", "center", return image_format
"-crop", "%s+0+0" % res, else:
"-sampling-factor", "2x2", raise ImageMagickError(f"ERROR: {identify.read_error()}")
"-interlace", "none",
"-set", "units", "PixelsPerSecond")

@ -1032,7 +1032,7 @@ class Package(metaclass=PackageCreator):
if file_md5 is None: if file_md5 is None:
try: try:
file_text = read_file(real_path).encode() file_text = read_file(real_path, binary=True)
except FilesError as error: except FilesError as error:
raise PackageError(str(error)) raise PackageError(str(error))
file_md5 = hashlib.md5(file_text).hexdigest() file_md5 = hashlib.md5(file_text).hexdigest()

@ -1,6 +1,14 @@
from calculate.variables.datavars import (Variable, Namespace, Dependence, from calculate.variables.datavars import (
StringType, BooleanType, HashType, Variable,
ListType, Calculate, Copy) Namespace,
Dependence,
StringType,
BooleanType,
HashType,
ListType,
Calculate,
Copy
)
from calculate.vars.main.os.func import get_arch_gentoo from calculate.vars.main.os.func import get_arch_gentoo
from calculate.vars.install.os.func import (get_audio_selected, from calculate.vars.install.os.func import (get_audio_selected,
get_available_audio_system) get_available_audio_system)
@ -10,6 +18,7 @@ def import_variables():
with Namespace("arch"): with Namespace("arch"):
Variable("machine", type=StringType, Variable("machine", type=StringType,
source=Copy("main.os.arch.machine")) source=Copy("main.os.arch.machine"))
Variable("gentoo", type=StringType, Variable("gentoo", type=StringType,
source=Calculate(get_arch_gentoo, ".machine")) source=Calculate(get_arch_gentoo, ".machine"))
@ -17,6 +26,7 @@ def import_variables():
Variable("available", type=ListType, Variable("available", type=ListType,
source=Calculate( source=Calculate(
get_available_audio_system)) get_available_audio_system))
Variable("selected", type=StringType, Variable("selected", type=StringType,
source=Calculate( source=Calculate(
get_audio_selected, get_audio_selected,
@ -26,12 +36,24 @@ def import_variables():
Variable("subsystem", type=StringType, source=Copy("main.os.subsystem")) Variable("subsystem", type=StringType, source=Copy("main.os.subsystem"))
with Namespace("container"): with Namespace("container"):
Variable("type", type=StringType, source=Copy("main.os.container.type")) Variable("type", type=StringType,
source=Copy("main.os.container.type"))
with Namespace("linux"): with Namespace("linux"):
Variable("shortname", type=StringType, source=Copy("main.os.linux.shortname")) Variable("shortname", type=StringType,
Variable("name", type=StringType, source=Copy("main.os.linux.name")) source=Copy("main.os.linux.shortname"))
Variable("subname", type=StringType, source=Copy("main.os.linux.subname"))
Variable("system", type=StringType, source=Copy("main.os.linux.system")) Variable("name", type=StringType,
Variable("ver", type=StringType, source=Copy("main.os.linux.ver")) source=Copy("main.os.linux.name"))
Variable("build", type=StringType, source=Copy("main.os.linux.build"))
Variable("subname", type=StringType,
source=Copy("main.os.linux.subname"))
Variable("system", type=StringType,
source=Copy("main.os.linux.system"))
Variable("ver", type=StringType,
source=Copy("main.os.linux.ver"))
Variable("build", type=StringType,
source=Copy("main.os.linux.build"))

@ -1,5 +1,6 @@
from calculate.utils.package import PackageAtomParser from calculate.utils.package import PackageAtomParser
def get_available_audio_system(): def get_available_audio_system():
audio_systems = ( audio_systems = (
('alsa', None), ('alsa', None),
@ -12,6 +13,7 @@ def get_available_audio_system():
if pkg is None or package_db.is_package_exists(pkg) if pkg is None or package_db.is_package_exists(pkg)
] ]
def get_audio_selected(available_systems, cmdline_audio): def get_audio_selected(available_systems, cmdline_audio):
available_systems = available_systems.value available_systems = available_systems.value

@ -26,3 +26,12 @@ Variable('cl_config_archive', type=StringType.readonly,
Variable('cl_exec_dir_path', type=StringType.readonly, Variable('cl_exec_dir_path', type=StringType.readonly,
source='/var/lib/calculate/.execute') source='/var/lib/calculate/.execute')
Variable('cl_resolutions', type=ListType.readonly,
source=["1680x1050", "1024x768"])
Variable('cl_resolutions', type=ListType.readonly,
source=["1680x1050", "1024x768"])
Variable('cl_image_formats', type=ListType.readonly,
source=["GIF", "JPG", "JPEG", "PNG", "GFXBOOT"])

@ -9,6 +9,7 @@ from calculate.variables.datavars import (
Calculate Calculate
) )
def get_ebuild_phase(): def get_ebuild_phase():
return os.environ.get("EBUILD_PHASE", "") return os.environ.get("EBUILD_PHASE", "")
@ -105,9 +106,9 @@ def get_isoscan_fullpath(base_path, filename):
def import_variables(): def import_variables():
Variable("chroot_path", type=StringType, Variable("chroot_path", type=StringType,
source=Calculate(lambda x:x.value, "main.cl_chroot_path")) source=Calculate(lambda x: x.value, "main.cl_chroot_path"))
Variable("root_path", type=StringType, Variable("root_path", type=StringType,
source=Calculate(lambda x:x.value, "main.cl_root_path")) source=Calculate(lambda x: x.value, "main.cl_root_path"))
Variable("ebuild_phase", type=StringType, Variable("ebuild_phase", type=StringType,
source=Calculate(get_ebuild_phase)) source=Calculate(get_ebuild_phase))

@ -1,4 +1,3 @@
import os import os
from calculate.utils.fs import readFile from calculate.utils.fs import readFile
from calculate.variables.datavars import Variable, Namespace, Dependence, \ from calculate.variables.datavars import Variable, Namespace, Dependence, \
@ -6,6 +5,7 @@ from calculate.variables.datavars import Variable, Namespace, Dependence, \
from calculate.utils.files import Process, FilesError from calculate.utils.files import Process, FilesError
import re import re
def get_resolution_by_xdpyinfo(): def get_resolution_by_xdpyinfo():
""" """
Get resolution by xdpyinfo utility Get resolution by xdpyinfo utility
@ -23,13 +23,17 @@ def get_resolution_by_xdpyinfo():
return "%sx%s" % (searchRes.group(1), searchRes.group(2)) return "%sx%s" % (searchRes.group(1), searchRes.group(2))
return "" return ""
def get_resolution_by_xorg(): def get_resolution_by_xorg():
pass pass
def get_standart_resolution(): def get_standart_resolution():
# TODO: заглушка # TODO: заглушка
return "1024x768" return "1024x768"
with Namespace('resolution'):
Variable("standard", type=StringType, def import_variables():
source=Calculate(get_standart_resolution)) with Namespace('resolution'):
Variable("standard", type=StringType,
source=Calculate(get_standart_resolution))

@ -7,13 +7,16 @@ from calculate.variables.datavars import (
TableType TableType
) )
def get_repository_table(): def get_repository_table():
return [ return [{'name': 'gentoo',
{'name':'gentoo', 'url':
'url': 'git://git.calculate-linux.org/calculate/gentoo-overlay.git'}, 'git://git.calculate-linux.org/calculate/gentoo-overlay.git'},
{'name':'calculate', {'name': 'calculate',
'url': 'git://git.calculate-linux.org/calculate/calculate-overlay.git'}, 'url':
] 'git://git.calculate-linux.org/calculate/calculate-overlay.git'},
]
def import_variables(): def import_variables():
Variable('repositories', type=TableType, Variable('repositories', type=TableType,

@ -163,18 +163,46 @@ def DictionariesWithoutSections():
return (OriginalDictionary, TemplateDictionary, ResultDictionary) return (OriginalDictionary, TemplateDictionary, ResultDictionary)
import pytest
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption( parser.addoption(
"--chroot-test", action="store_true", default=False, help="run chroot tests" "--chroot-test",
action="store_true",
default=False,
help="run chroot tests"
)
parser.addoption(
"--backgrounds-slow",
action="store_true",
default=False,
help="run slow backgrounds tests."
) )
parser.addoption(
"--from-root",
action="store_true",
default=False,
help="run tests that needs root rights."
)
def pytest_collection_modifyitems(config, items): def pytest_collection_modifyitems(config, items):
if config.getoption("--chroot-test"): if not config.getoption("--chroot-test"):
return skip_chroot = pytest.mark.skip(
skip_chroot = pytest.mark.skip(reason="need --chroot option to run") reason="need --chroot-test option to run")
for item in items: for item in items:
if "chroot" in item.keywords: if "chroot" in item.keywords:
item.add_marker(skip_chroot) item.add_marker(skip_chroot)
if not config.getoption("--backgrounds-slow"):
skip_backgrounds = pytest.mark.skip(
reason="need --backgrounds-slow option to run")
for item in items:
if "backgrounds_slow" in item.keywords:
item.add_marker(skip_backgrounds)
if not config.getoption("--from-root"):
skip_from_root = pytest.mark.skip(
reason="need --from-root option to run")
for item in items:
if "needs_root" in item.keywords:
item.add_marker(skip_from_root)

@ -3,6 +3,8 @@
[pytest] [pytest]
markers = markers =
base: marker for running tests for base format class. base: marker for running tests for base format class.
backgrounds: marker for running tests for backgrounds format.
backgrounds_slow: marker for running slow tests for backgrounds format.
bind: marker for running tests for bind format. bind: marker for running tests for bind format.
compiz: marker for running tests for compiz format. compiz: marker for running tests for compiz format.
contents: marker for running tests for contents format. contents: marker for running tests for contents format.
@ -44,3 +46,4 @@ markers =
server: marker for testing of the server. server: marker for testing of the server.
chroot: marker for testing running by chroot chroot: marker for testing running by chroot
needs_root: marker for tests that needs root rights.

@ -0,0 +1,331 @@
import os
import pytest
import shutil
from calculate.templates.format.base_format import FormatError
from calculate.templates.format.backgrounds_format import BackgroundsFormat
from calculate.templates.template_engine import ParametersContainer
from calculate.variables.datavars import NamespaceNode, VariableNode, ListType
datavars = NamespaceNode("<root>")
datavars.add_namespace(NamespaceNode("main"))
VariableNode("cl_resolutions", datavars.main, ListType,
source=["1024x768"])
TMP_BACKUP_DIR = 'tests/templates/format/testfiles/backgrounds/tmp.backup'
TMP_DIR = 'tests/templates/format/testfiles/backgrounds/tmp'
@pytest.mark.backgrounds
def test_make_tmp_dir():
if not os.path.exists(TMP_DIR):
shutil.copytree(TMP_BACKUP_DIR, TMP_DIR)
@pytest.mark.backgrounds
@pytest.mark.parametrize('case', [
{
"id": "PNG -> PNG",
"source":
'tests/templates/format/testfiles/backgrounds/picture_0.png',
"convert": False,
"stretch": True,
"target": 'result_0-',
"template_text": ("original\n1024x768"),
"result": ("PNG", ["32x16", "1024x768"]),
},
{
"id": "JPEG -> JPEG",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": False,
"stretch": True,
"target": 'result_1-',
"template_text": ("original\n1024x768"),
"result": ("JPEG", ["320x180", "1024x768"]),
},
{
"id": "PNG -> JPEG",
"source":
'tests/templates/format/testfiles/backgrounds/picture_0.png',
"convert": "JPEG",
"stretch": True,
"target": "result_2-",
"template_text": ("320x160\n1024x768"),
"result": ("JPEG", ["320x160", "1024x768"]),
},
{
"id": "PNG -> GIF",
"source":
'tests/templates/format/testfiles/backgrounds/picture_0.png',
"convert": "GIF",
"stretch": True,
"target": "result_3-",
"template_text": ("320x160\n1024x768"),
"result": ("GIF", ["320x160", "1024x768"]),
},
pytest.param(
{
"id": "JPEG -> PNG",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": "PNG",
"stretch": True,
"target": "result_4-",
"template_text": ("640x360\n1024x768"),
"result": ("PNG", ["640x360", "1024x768"]),
},
marks=pytest.mark.backgrounds_slow
),
{
"id": "JPEG -> GIF",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": "GIF",
"stretch": True,
"target": "result_5-",
"template_text": ("original\n1024x768"),
"result": ("GIF", ["320x180", "1024x768"]),
},
{
"id": "JPEG -> GFXBOOT(JPEG)",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": "GFXBOOT",
"stretch": False,
"target": "result_6-",
"template_text": ("original\n200x100"),
"result": ("JPEG", ["320x180", "200x100"]),
},
{
"id": "PNG -> JPEG (single from template)",
"source":
'tests/templates/format/testfiles/backgrounds/picture_0.png',
"convert": "JPEG",
"stretch": True,
"target": "result_7",
"template_text": ("320x160"),
"result": ("JPEG", ["320x160"]),
},
{
"id": "JPEG -> JPEG (single from variable)",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": False,
"stretch": True,
"target": "result_8-",
"template_text": "",
"result": ("JPEG", ["1024x768"]),
},
],
ids=lambda x: x["id"])
def test_resize_and_convert_of_different_formats(case):
target_path = os.path.join(TMP_DIR, case["target"])
parameters = ParametersContainer({'source': case["source"],
'convert': case["convert"],
'stretch': case["stretch"]})
backgrounds_object = BackgroundsFormat(case["template_text"],
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
images_format, resolutions = case["result"]
output_paths = []
if len(resolutions) > 1 or not case['template_text']:
for resolution in resolutions:
output_paths.append("{}{}.{}".format(target_path, resolution,
images_format.lower()))
elif len(resolutions) == 1:
resolution = next(iter(resolutions))
output_paths.append(target_path)
for outpath in output_paths:
assert os.path.exists(outpath)
assert backgrounds_object._magician.get_image_format(outpath
) == images_format
# print("CHANGED FILES:")
# for path, status in changed_files.items():
# print(f"{path} -> {status}")
# assert False
@pytest.mark.backgrounds
@pytest.mark.parametrize('case', [
{
"id": "stretch off",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": False,
"stretch": False,
"target": 'result_8-',
"template_text": ("400x200\n30x10"),
"result": {"400x200": False, "30x10": True},
},
{
"id": "stretch on",
"source":
'tests/templates/format/testfiles/backgrounds/picture_1.jpg',
"convert": False,
"stretch": True,
"target": 'result_9-',
"template_text": ("400x200\n30x10"),
"result": {"400x200": True, "30x10": True},
},
],
ids=lambda x: x["id"])
def test_scretch_parameter(case):
target_path = os.path.join(TMP_DIR, case["target"])
parameters = ParametersContainer({'source': case["source"],
'convert': case["convert"],
'stretch': case["stretch"]})
backgrounds_object = BackgroundsFormat(case["template_text"],
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
for resolution, result in case["result"].items():
image_path = "{}{}.jpeg".format(target_path, resolution)
assert os.path.exists(image_path) is result
if result:
width, height = resolution.split("x")
assert backgrounds_object._magician.get_image_resolution(
image_path) == (
int(width),
int(height))
# @pytest.mark.backgrounds
# def test_to_create():
# target_path = os.path.join(TMP_BACKUP_DIR, "result_13-")
# source_path = 'tests/templates/format/testfiles/backgrounds/picture_2.png'
# parameters = ParametersContainer({'source': source_path,
# 'convert': False,
# 'stretch': False})
# backgrounds_object = BackgroundsFormat("1650x1050\n1024x768",
# "path/to/template",
# parameters, datavars)
# backgrounds_object.execute_format(target_path)
@pytest.mark.backgrounds
def test_if_template_has_been_already_used__it_will_not_be_reused():
target_path = os.path.join(TMP_DIR, "result_10-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_2.png'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False})
backgrounds_object = BackgroundsFormat("1650x1050\n1024x768",
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
assert not changed_files
@pytest.mark.backgrounds_slow
@pytest.mark.backgrounds
def test_if_template_is_using_in_directory_but_other_template_has_been_already_used_for_the_same_image_name__latest_template_will_remove_old_images_and_md5sum_file_and_create_new_ones():
target_path = os.path.join(TMP_DIR, "result_11-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_2.png'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False})
backgrounds_object = BackgroundsFormat("1440x1080\n1280x960",
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
assert not os.path.exists(f"{target_path}1650x1050.png")
assert os.path.exists(f"{target_path}1440x1080.png")
assert os.path.exists(f"{target_path}1280x960.png")
assert not os.path.exists(f"{target_path}1024x768.png")
assert changed_files == {f"{target_path}1650x1050.png": "D",
f"{target_path}1024x768.png": "D",
f"{target_path.strip('_-.')}.md5sum": "M",
f"{target_path}1440x1080.png": "N",
f"{target_path}1280x960.png": "N"}
@pytest.mark.backgrounds_slow
@pytest.mark.backgrounds
def test_if_template_is_using_in_directory_which_contains_md5sum_file_without_any_images__the_template_will_remove_md5sum_file_and_create_new_one_and_images():
target_path = os.path.join(TMP_DIR, "result_12-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_2.png'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False})
backgrounds_object = BackgroundsFormat("1440x1080\n1280x960",
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
assert os.path.exists(f"{target_path}1440x1080.png")
assert os.path.exists(f"{target_path}1280x960.png")
assert changed_files == {f"{target_path.strip('_-.')}.md5sum": "M",
f"{target_path}1440x1080.png": "N",
f"{target_path}1280x960.png": "N"}
@pytest.mark.backgrounds_slow
@pytest.mark.backgrounds
def test_if_template_is_using_in_directory_which_contains_some_image_files_without_md5sum_file__the_template_will_remove_all_images_and_create_new_ones_and_new_md5sum_file():
target_path = os.path.join(TMP_DIR, "result_13-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_2.png'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False})
backgrounds_object = BackgroundsFormat("1440x1080\n1280x960",
"path/to/template",
parameters, datavars)
changed_files = backgrounds_object.execute_format(target_path)
assert not os.path.exists(f"{target_path}1650x1050.png")
assert os.path.exists(f"{target_path}1440x1080.png")
assert os.path.exists(f"{target_path}1280x960.png")
assert not os.path.exists(f"{target_path}1024x768.png")
assert changed_files == {f"{target_path}1650x1050.png": "D",
f"{target_path}1024x768.png": "D",
f"{target_path.strip('_-.')}.md5sum": "N",
f"{target_path}1440x1080.png": "N",
f"{target_path}1280x960.png": "N"}
@pytest.mark.backgrounds
def test_if_template_s_name_parameter_is_the_empty_line_and_its_text_contents_two_or_more_resolution_values__the_output_files_name_will_contains_its_resolutions_only():
target_path = os.path.join(TMP_DIR, "result_14-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_1.jpg'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False,
'name': ''})
backgrounds_object = BackgroundsFormat("100x100\n60x60",
"path/to/template",
parameters, datavars)
backgrounds_object.execute_format(target_path)
assert os.path.exists(os.path.join(TMP_DIR, "100x100.jpeg"))
assert os.path.exists(os.path.join(TMP_DIR, "60x60.jpeg"))
assert os.path.exists(os.path.join(TMP_DIR, "md5sum"))
@pytest.mark.backgrounds
def test_if_template_s_name_parameter_is_the_empty_line_and_its_text_contents_one_resolution_value__the_format_will_raise_FormatError_exception():
target_path = os.path.join(TMP_DIR, "result_15-")
source_path = 'tests/templates/format/testfiles/backgrounds/picture_1.jpg'
parameters = ParametersContainer({'source': source_path,
'convert': False,
'stretch': False,
'name': ''})
backgrounds_object = BackgroundsFormat("100x100",
"path/to/template",
parameters, datavars)
with pytest.raises(FormatError):
backgrounds_object.execute_format(target_path)
@pytest.mark.backgrounds
def test_remove_tmp():
if os.path.exists(TMP_DIR):
shutil.rmtree(TMP_DIR)

@ -3,6 +3,8 @@ import pytest
import shutil import shutil
from calculate.templates.format.contents_format import ContentsFormat from calculate.templates.format.contents_format import ContentsFormat
from calculate.utils.package import PackageAtomName, Version, Package from calculate.utils.package import PackageAtomName, Version, Package
from calculate.templates.template_engine import ParametersContainer
from calculate.variables.datavars import NamespaceNode
from calculate.utils.files import join_paths from calculate.utils.files import join_paths
@ -54,7 +56,9 @@ sym /etc/dir_3/link_0 -> ../dir_1/file_0 {2}
int(os.lstat(join_paths(chroot_path, int(os.lstat(join_paths(chroot_path,
'/etc/dir_3/link_0')).st_mtime)) '/etc/dir_3/link_0')).st_mtime))
contents_format = ContentsFormat(template_text, '/path/to/template') contents_format = ContentsFormat(template_text, '/path/to/template',
ParametersContainer(),
NamespaceNode())
contents_format.execute_format('target/path', chroot_path=chroot_path) contents_format.execute_format('target/path', chroot_path=chroot_path)
test_package = Package(test_package_name, test_package = Package(test_package_name,
@ -101,7 +105,9 @@ dir /etc/dir_1
dir /etc/dir_3 dir /etc/dir_3
''' '''
contents_format = ContentsFormat(template_text, '/path/to/template') contents_format = ContentsFormat(template_text, '/path/to/template',
ParametersContainer(),
NamespaceNode())
contents_format.execute_format('target/path', chroot_path=chroot_path) contents_format.execute_format('target/path', chroot_path=chroot_path)
test_package = Package(test_package_name, test_package = Package(test_package_name,
@ -177,7 +183,9 @@ dir /etc/dir_5
sym /etc/dir_5/link_0 -> ../dir_1/file_0 1601991426 sym /etc/dir_5/link_0 -> ../dir_1/file_0 1601991426
''' '''
contents_format = ContentsFormat(template_text, '/path/to/template') contents_format = ContentsFormat(template_text, '/path/to/template',
ParametersContainer(),
NamespaceNode())
contents_format.execute_format('target/path', chroot_path=chroot_path) contents_format.execute_format('target/path', chroot_path=chroot_path)
test_package = Package(test_package_name, test_package = Package(test_package_name,

@ -1,6 +1,8 @@
import pytest import pytest
import shutil import shutil
from calculate.templates.format.patch_format import PatchFormat from calculate.templates.format.patch_format import PatchFormat
from calculate.templates.template_engine import ParametersContainer
from calculate.variables.datavars import NamespaceNode
from calculate.utils.files import Process from calculate.utils.files import Process
from os import path from os import path
import os import os
@ -31,7 +33,9 @@ class TestExecuteMethods:
with open(path.join(cwd_path, 'diff_1.patch')) as patch_file: with open(path.join(cwd_path, 'diff_1.patch')) as patch_file:
patch_text = patch_file.read() patch_text = patch_file.read()
diff_patch = PatchFormat(patch_text, '/template/path') diff_patch = PatchFormat(patch_text, '/template/path',
ParametersContainer(),
NamespaceNode("<root>"))
output = diff_patch.execute_format(target_path=cwd_path) output = diff_patch.execute_format(target_path=cwd_path)
if output: if output:
for changed_file, change_type in diff_patch.changed_files.items(): for changed_file, change_type in diff_patch.changed_files.items():
@ -62,7 +66,9 @@ class TestExecuteMethods:
with open(path.join(patch_path)) as patch_file: with open(path.join(patch_path)) as patch_file:
patch_text = patch_file.read() patch_text = patch_file.read()
diff_patch = PatchFormat(patch_text, '/template/path') diff_patch = PatchFormat(patch_text, '/template/path',
ParametersContainer(),
NamespaceNode("<root>"))
diff_patch.execute_format(target_path=cwd_path) diff_patch.execute_format(target_path=cwd_path)
for changed_file, change_type in diff_patch.changed_files.items(): for changed_file, change_type in diff_patch.changed_files.items():

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@ -0,0 +1,2 @@
{% calculate append = "skip", action = "install",
package = "test-category/test-package" %}

@ -0,0 +1,3 @@
{% calculate format = "backgrounds", source = "/etc/picture.jpg", name = "image-" %}
original
300x100

@ -0,0 +1,2 @@
{% calculate append = "skip", action = "install",
package = "test-category/test-package" %}

@ -0,0 +1,4 @@
{% calculate format = "backgrounds", source = "/etc/picture.jpg", name = "image-",
stretch %}
666x333
220x130

@ -0,0 +1,2 @@
{% calculate append = "skip", action = "install",
package = "test-category/test-package" %}

@ -0,0 +1,3 @@
{% calculate format = "backgrounds", source = "/etc/picture.jpg", name = "image.png",
stretch, convert = "png" %}
300x200

@ -0,0 +1,2 @@
{% calculate append = "skip", action = "install",
package = "test-category/test-package" %}

@ -0,0 +1,4 @@
{% calculate format = "backgrounds", source = "/etc/picture.jpg", name = "image-",
stretch %}
original
300x100

@ -3,20 +3,20 @@ from calculate.utils.images import ImageMagick, ImageMagickError
import os import os
from calculate.utils.files import Process from calculate.utils.files import Process
@pytest.mark.images_utils @pytest.mark.images_utils
def test_imagemagick_initialization(): def test_imagemagick_initialization():
im = ImageMagick() im = ImageMagick()
assert im.available assert im.available
assert not im.chroot assert not im.chrooted
im = ImageMagick(prefix='/mnt/somepath') im = ImageMagick(chroot='/mnt/somepath')
assert not im.available assert not im.available
assert im.chroot assert im.chrooted
@pytest.mark.images_utils @pytest.mark.images_utils
@pytest.mark.parametrize('case', @pytest.mark.parametrize('case', [
[
{ {
"name": "simple path", "name": "simple path",
"chroot": "/mnt/install", "chroot": "/mnt/install",
@ -42,23 +42,23 @@ def test_imagemagick_initialization():
"result": None, "result": None,
}, },
], ],
ids=lambda x:x["name"]) ids=lambda x: x["name"])
def test_imagemagick_trim_prefix_path(case): def test_imagemagick_trim_prefix_path(case):
im = ImageMagick(case['chroot']) im = ImageMagick(case['chroot'])
assert im.trim_prefix_path(case["source"]) == case["result"] assert im.trim_prefix_path(case["source"]) == case["result"]
@pytest.mark.images_utils @pytest.mark.images_utils
@pytest.mark.parametrize('case', @pytest.mark.parametrize('case', [
[
{ {
"name": "PNG file", "name": "PNG file",
"image": "tests/utils/testfiles/file.png", "image": "tests/utils/testfiles/file.png",
"result": (48,48), "result": (48, 48),
}, },
{ {
"name": "JPEG file", "name": "JPEG file",
"image": "tests/utils/testfiles/file.jpg", "image": "tests/utils/testfiles/file.jpg",
"result": (320,180), "result": (320, 180),
}, },
{ {
"name": "No file", "name": "No file",
@ -71,10 +71,48 @@ def test_imagemagick_trim_prefix_path(case):
"result": None, "result": None,
}, },
], ],
ids=lambda x:x["name"]) ids=lambda x: x["name"])
def test_imagemagick_get_resolutions(case): def test_imagemagick_get_resolutions(case):
im = ImageMagick() im = ImageMagick()
assert im.get_image_resolution(case["image"]) == case["result"] if case["result"] is None:
with pytest.raises(ImageMagickError):
im.get_image_resolution(case["image"])
else:
assert im.get_image_resolution(case["image"]) == case["result"]
@pytest.mark.images_utils
@pytest.mark.parametrize('case', [
{
"name": "PNG file",
"image": "tests/utils/testfiles/file.png",
"result": "PNG",
},
{
"name": "JPEG file",
"image": "tests/utils/testfiles/file.jpg",
"result": "JPEG",
},
{
"name": "No file",
"image": "tests/utils/testfiles/file2.jpg",
"result": None,
},
{
"name": "Wrong file",
"image": "tests/utils/testfiles/wrong.jpg",
"result": None,
},
],
ids=lambda x: x["name"])
def test_imagemagick_get_image_format(case):
im = ImageMagick()
if case["result"] is None:
with pytest.raises(ImageMagickError):
im.get_image_format(case["image"])
else:
assert im.get_image_format(case["image"]) == case["result"]
@pytest.fixture @pytest.fixture
def chroot_test(): def chroot_test():
@ -88,29 +126,37 @@ def chroot_test():
finally: finally:
os.unlink(chrootpath) os.unlink(chrootpath)
@pytest.mark.chroot @pytest.mark.chroot
@pytest.mark.images_utils @pytest.mark.images_utils
def test_chroot_imagemagick_get_resolution(chroot_test): def test_chroot_imagemagick_get_resolution(chroot_test):
im = ImageMagick(chroot_test) im = ImageMagick(chroot_test)
curpath = os.getcwd() curpath = os.getcwd()
image_path = os.path.join(curpath, "tests/utils/testfiles/file.png") image_path = os.path.join(curpath, "tests/utils/testfiles/file.png")
assert im.get_image_resolution(image_path) == (48,48) assert im.get_image_resolution(image_path) == (48, 48)
def get_histogram(image, remap_image): def get_histogram(image, remap_image):
p = Process("/usr/bin/convert", image, "-remap", remap_image, "-format", "%c", "histogram:info:-") p = Process("/usr/bin/convert", image, "-remap",
remap_image, "-format", "%c", "histogram:info:-")
return p.read() return p.read()
def get_verbose_image_info(image): def get_verbose_image_info(image):
p = Process("/usr/bin/identify", "-verbose", image) p = Process("/usr/bin/identify", "-verbose", image)
return p.read() return p.read()
def discard_space(x: str) -> str:
return x.replace(" ", "").replace("\n", "")
@pytest.mark.images_utils @pytest.mark.images_utils
@pytest.mark.parametrize('case', @pytest.mark.parametrize('case', [
[
{ {
# проверка, пропорционального уменьшения # проверка, пропорционального уменьшения
"name": "Origin test", "name": "Origin test",
"resize": (16,32), "resize": (16, 32),
"result": """192: (0,0,0) #000000 black "result": """192: (0,0,0) #000000 black
64: (0,255,0) #00FF00 lime 64: (0,255,0) #00FF00 lime
256: (255,255,255) #FFFFFF white""" 256: (255,255,255) #FFFFFF white"""
@ -120,22 +166,23 @@ def get_verbose_image_info(image):
# удаляются только части изображения справа и слева # удаляются только части изображения справа и слева
# в исходном изображении на этих частях находится белый фон # в исходном изображении на этих частях находится белый фон
"name": "Shrink horizontal", "name": "Shrink horizontal",
"resize": (16,16), "resize": (16, 16),
"result": """192: (0,0,0) #000000 black "result": """192: (0,0,0) #000000 black
64: (0,255,0) #00FF00 lime""" 64: (0,255,0) #00FF00 lime"""
}, },
{ {
# проверка, что при уменьшении изображения первоначально оно сдавливается # проверка, что при уменьшении изображения первоначально оно
# по вертикали а затем обрезаются части слева и справа # сдавливается # по вертикали а затем обрезаются части слева и
# справа
"name": "Shrink all", "name": "Shrink all",
"resize": (8,8), "resize": (8, 8),
"result": """48: (0,0,0) #000000 black "result": """48: (0,0,0) #000000 black
16: (0,255,0) #00FF00 lime""" 16: (0,255,0) #00FF00 lime"""
}, },
{ {
# проверка, пропорционального уменьшения # проверка, пропорционального уменьшения
"name": "Shrink proportionately", "name": "Shrink proportionately",
"resize": (8,16), "resize": (8, 16),
"result": """48: (0,0,0) #000000 black "result": """48: (0,0,0) #000000 black
16: (0,255,0) #00FF00 lime 16: (0,255,0) #00FF00 lime
64: (255,255,255) #FFFFFF white""" 64: (255,255,255) #FFFFFF white"""
@ -143,7 +190,7 @@ def get_verbose_image_info(image):
{ {
# проверка, пропорционального увеличения # проверка, пропорционального увеличения
"name": "Increase size proportionately", "name": "Increase size proportionately",
"resize": (32,64), "resize": (32, 64),
"result": """768: (0,0,0) #000000 black "result": """768: (0,0,0) #000000 black
256: (0,255,0) #00FF00 lime 256: (0,255,0) #00FF00 lime
1024: (255,255,255) #FFFFFF white""" 1024: (255,255,255) #FFFFFF white"""
@ -151,7 +198,7 @@ def get_verbose_image_info(image):
{ {
# проверка увеличения и обрезки по горизонтали # проверка увеличения и обрезки по горизонтали
"name": "Increase size and cut", "name": "Increase size and cut",
"resize": (32,32), "resize": (32, 32),
"result": """768: (0,0,0) #000000 black "result": """768: (0,0,0) #000000 black
256: (0,255,0) #00FF00 lime""" 256: (0,255,0) #00FF00 lime"""
}, },
@ -160,24 +207,27 @@ def get_verbose_image_info(image):
# в этом случае будет отрезан верх и низ # в этом случае будет отрезан верх и низ
# поэтому на выходе нет зелёного цвета # поэтому на выходе нет зелёного цвета
"name": "Increase horizontal size", "name": "Increase horizontal size",
"resize": (16,48), "resize": (16, 48),
"result": """384: (0,0,0) #000000 gray(0) "result": """384: (0,0,0) #000000 gray(0)
384: (255,255,255) #FFFFFF gray(255)""" 384: (255,255,255) #FFFFFF gray(255)"""
}, },
], ],
ids=lambda x:x["name"]) ids=lambda x: x["name"])
def test_imagemagick_convert(case): def test_imagemagick_convert(case):
image_path = "tests/utils/testfiles/origin.png" image_path = "tests/utils/testfiles/origin.png"
output_file = "tests/utils/testfiles/test_output5.png" output_file = "tests/utils/testfiles/test_output5.png"
if os.path.exists(output_file): if os.path.exists(output_file):
os.unlink(output_file) os.unlink(output_file)
im = ImageMagick() im = ImageMagick()
im.default_opts = ["-filter","box"] im.default_opts = ["-filter", "box"]
assert im.convert_resize_crop_center(image_path, output_file, *case["resize"]) assert im.convert_resize_crop_center(image_path, output_file,
*case["resize"])
histogram = get_histogram(output_file, image_path) histogram = get_histogram(output_file, image_path)
discard_space = lambda x: x.replace(" ","").replace("\n","")
assert discard_space(histogram) == discard_space(case["result"]) assert discard_space(histogram) == discard_space(case["result"])
@pytest.mark.chroot @pytest.mark.chroot
@pytest.mark.images_utils @pytest.mark.images_utils
def test_chroot_imagemagick_convert_center(chroot_test): def test_chroot_imagemagick_convert_center(chroot_test):
@ -191,12 +241,15 @@ def test_chroot_imagemagick_convert_center(chroot_test):
if os.path.exists(output_file): if os.path.exists(output_file):
os.unlink(output_file) os.unlink(output_file)
im = ImageMagick(chroot_test) im = ImageMagick(chroot_test)
im.default_opts = ["-filter","box"] im.default_opts = ["-filter", "box"]
assert im.convert_resize_crop_center(image_path, output_file, 8, 8) assert im.convert_resize_crop_center(image_path, output_file, 8, 8)
histogram = get_histogram(output_file, image_path) histogram = get_histogram(output_file, image_path)
discard_space = lambda x: x.replace(" ","").replace("\n","")
assert discard_space(histogram) == discard_space(result) assert discard_space(histogram) == discard_space(result)
@pytest.mark.images_utils @pytest.mark.images_utils
def test_imagemagick_convert_gfxboot(): def test_imagemagick_convert_gfxboot():
output_file = "tests/utils/testfiles/test_output.jpg" output_file = "tests/utils/testfiles/test_output.jpg"
@ -205,11 +258,12 @@ def test_imagemagick_convert_gfxboot():
im.convert_resize_gfxboot(image_path, output_file, 32, 32) im.convert_resize_gfxboot(image_path, output_file, 32, 32)
assert "sampling-factor: 2x2" in get_verbose_image_info(output_file) assert "sampling-factor: 2x2" in get_verbose_image_info(output_file)
@pytest.mark.images_utils @pytest.mark.images_utils
def test_clear_imagemagick_convert(): def test_clear_imagemagick_convert():
for output_file in ( for output_file in (
"tests/utils/testfiles/test_output.png", "tests/utils/testfiles/test_output.png",
"tests/utils/testfiles/test_output.jpg" "tests/utils/testfiles/test_output.jpg"
): ):
if os.path.exists(output_file): if os.path.exists(output_file):
os.unlink(output_file) os.unlink(output_file)

@ -74,8 +74,8 @@ class TestDatavars:
assert namespace_1._namespaces == dict() assert namespace_1._namespaces == dict()
assert namespace_1._variables == {'var_1': variable_1, assert namespace_1._variables == {'var_1': variable_1,
'var_2': variable_2, 'var_2': variable_2,
'name': variable_3} 'name': variable_3}
assert namespace_1.get_fullname() == 'namespace_1' assert namespace_1.get_fullname() == 'namespace_1'
assert namespace_1.var_1 == 'value_1' assert namespace_1.var_1 == 'value_1'

Loading…
Cancel
Save