Compare commits
No commits in common. "master" and "dev" have entirely different histories.
84 changed files with 3767 additions and 1298 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
**/__pycache__
|
||||
.idea/*
|
||||
__pycache__
|
||||
main.py
|
||||
test.py
|
||||
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Maximilian Keßler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
0
PyTeX/__init__.py
Normal file
0
PyTeX/__init__.py
Normal file
0
PyTeX/build/__init__.py
Normal file
0
PyTeX/build/__init__.py
Normal file
2
PyTeX/build/build/__init__.py
Normal file
2
PyTeX/build/build/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .build_dir_spec import BuildDirConfig
|
||||
from .builder import PyTeXBuilder
|
99
PyTeX/build/build/build_dir_spec.py
Normal file
99
PyTeX/build/build/build_dir_spec.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from pathlib import Path, PurePath
|
||||
from typing import Optional, Dict
|
||||
|
||||
from .constants import *
|
||||
from ...format.config import Config
|
||||
|
||||
|
||||
class BuildDirConfig(Config):
|
||||
def __init__(
|
||||
self,
|
||||
source_root: Optional[Path] = None,
|
||||
build_root: Optional[Path] = None,
|
||||
doc_root: Optional[Path] = None,
|
||||
tex_root: Optional[Path] = None,
|
||||
wrapper_dir: Optional[PurePath] = None
|
||||
):
|
||||
self._pytex_source_root: Optional[Path] = source_root
|
||||
self._build_root: Optional[Path] = build_root
|
||||
self._doc_root: Optional[Path] = doc_root
|
||||
self._tex_source_root: Optional[Path] = tex_root
|
||||
self._wrapper_dir: Optional[PurePath] = wrapper_dir
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content = self._fill_keys(content)
|
||||
|
||||
self._tex_source_root = Path(filled_content[YAML_TEX_SOURCE_ROOT]) \
|
||||
if filled_content[YAML_TEX_SOURCE_ROOT] else None
|
||||
self._pytex_source_root = Path(filled_content[YAML_PYTEX_SOURCE_ROOT]) \
|
||||
if filled_content[YAML_PYTEX_SOURCE_ROOT] else None
|
||||
self._build_root = Path(filled_content[YAML_BUILD_ROOT]) \
|
||||
if filled_content[YAML_BUILD_ROOT] else None
|
||||
self._doc_root = Path(filled_content[YAML_DOC_ROOT]) \
|
||||
if filled_content[YAML_DOC_ROOT] else None
|
||||
self._wrapper_dir = Path(filled_content[YAML_WRAPPER_DIR]) \
|
||||
if filled_content[YAML_WRAPPER_DIR] else None
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
YAML_WRAPPER_DIR: self._wrapper_dir,
|
||||
YAML_BUILD_ROOT: self._build_root,
|
||||
YAML_DOC_ROOT: self._doc_root,
|
||||
YAML_TEX_SOURCE_ROOT: self._tex_source_root,
|
||||
YAML_PYTEX_SOURCE_ROOT: self._pytex_source_root
|
||||
}
|
||||
|
||||
@property
|
||||
def build_root(self) -> Path:
|
||||
if self._build_root is None:
|
||||
return Path('build')
|
||||
else:
|
||||
return self._build_root
|
||||
|
||||
@build_root.setter
|
||||
def build_root(self, build_root: Path):
|
||||
self._build_root = build_root
|
||||
|
||||
@property
|
||||
def doc_root(self) -> Path:
|
||||
if self._doc_root is None:
|
||||
return Path('build/doc')
|
||||
else:
|
||||
return self._doc_root
|
||||
|
||||
@doc_root.setter
|
||||
def doc_root(self, doc_root: Path):
|
||||
self._doc_root = doc_root
|
||||
|
||||
@property
|
||||
def pytex_source_root(self) -> Path:
|
||||
if self._pytex_source_root is None:
|
||||
return Path('src')
|
||||
else:
|
||||
return self._pytex_source_root
|
||||
|
||||
@pytex_source_root.setter
|
||||
def pytex_source_root(self, pytex_source_root: Path):
|
||||
self._pytex_source_root = pytex_source_root
|
||||
|
||||
@property
|
||||
def tex_source_root(self) -> Path:
|
||||
if self._tex_source_root is None:
|
||||
return Path('build/source')
|
||||
else:
|
||||
return self._tex_source_root
|
||||
|
||||
@tex_source_root.setter
|
||||
def tex_source_root(self, tex_source_root: Path):
|
||||
self._tex_source_root = tex_source_root
|
||||
|
||||
@property
|
||||
def wrapper_dir(self) -> PurePath:
|
||||
if self._wrapper_dir is None:
|
||||
return Path('')
|
||||
else:
|
||||
return self._wrapper_dir
|
||||
|
||||
@wrapper_dir.setter
|
||||
def wrapper_dir(self, wrapper_dir: Path):
|
||||
self._wrapper_dir = wrapper_dir
|
370
PyTeX/build/build/builder.py
Normal file
370
PyTeX/build/build/builder.py
Normal file
|
@ -0,0 +1,370 @@
|
|||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, List, Tuple, Set
|
||||
import shutil
|
||||
|
||||
from .enums import PyTeXRootDirType
|
||||
from .pytex_config import PyTeXConfig
|
||||
from ...exceptions import PyTeXException
|
||||
from ...format.errors import PyTeXError
|
||||
from ...format.formatting_config import FormattingConfig
|
||||
from ...logger import logger
|
||||
from .constants import *
|
||||
from ..versioning.version_info.version_info import VersionInfo, FileVersionInfo
|
||||
from .pytex_file import PyTeXSourceFile
|
||||
from .relative_path import RelativePath
|
||||
from .hashing import md5
|
||||
from .pytex_output_file import PyTeXOutputFile
|
||||
from PyTeX.format.git_version_info import GitVersionInfo
|
||||
from ..versioning.git.get_version_info import get_repo_status_info_from_file
|
||||
|
||||
|
||||
class PyTeXBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
pytex_config: Optional[Union[PyTeXConfig, Path, str]] = None,
|
||||
root_dir: Optional[Path] = None
|
||||
):
|
||||
if isinstance(pytex_config, Path) or isinstance(pytex_config, str):
|
||||
config_file = Path(pytex_config)
|
||||
self._pytex_config: Optional[PyTeXConfig] = PyTeXConfig.from_yaml(config_file)
|
||||
else:
|
||||
self._pytex_config = pytex_config
|
||||
if root_dir is None:
|
||||
if isinstance(pytex_config, Path):
|
||||
self._root_dir = pytex_config.parent
|
||||
else:
|
||||
self._root_dir = Path('.') # Working directory
|
||||
else:
|
||||
self._root_dir = root_dir
|
||||
|
||||
# Non-public attributes
|
||||
self._build_target_type: Optional[PyTeXRootDirType] = None
|
||||
self._old_version_info: Optional[VersionInfo] = None
|
||||
self._new_version_info: VersionInfo = VersionInfo()
|
||||
self._output_files: List[PyTeXOutputFile] = []
|
||||
self._pytex_files: List[PyTeXSourceFile] = []
|
||||
self._files_to_clean: Set[RelativePath] = set()
|
||||
self._files_to_overwrite: Set[RelativePath] = set()
|
||||
self._files_to_build: Set[PyTeXSourceFile] = set()
|
||||
self._tmp_dir: Path = self._root_dir / '.pytex'
|
||||
self._git_version_info: GitVersionInfo = self._foo()
|
||||
self._new_config: PyTeXConfig = PyTeXConfig()
|
||||
pass
|
||||
|
||||
def _foo(self):
|
||||
version = GitVersionInfo()
|
||||
version.repo_version = get_repo_status_info_from_file(
|
||||
self._root_dir
|
||||
)
|
||||
version.pytex_version = get_repo_status_info_from_file(
|
||||
Path(__file__)
|
||||
)
|
||||
return version
|
||||
|
||||
def _ignored_subfolders_in_build_dir(self) -> List[Path]:
|
||||
paths = []
|
||||
if self._build_target_type == PyTeXRootDirType.BUILD:
|
||||
dirs = [
|
||||
self.pytex_config.build_dir_specification.tex_source_root,
|
||||
self.pytex_config.build_dir_specification.doc_root
|
||||
]
|
||||
paths = [
|
||||
path.absolute().resolve() for path in dirs
|
||||
]
|
||||
paths.append(
|
||||
(self.target_root / '.git').absolute().resolve()
|
||||
)
|
||||
return paths
|
||||
|
||||
def is_ignored_in_build_dir(self, path: RelativePath) -> bool:
|
||||
real_path: Path = path.absolute().resolve()
|
||||
ignored_dirs = self._ignored_subfolders_in_build_dir()
|
||||
for ignored_dir in ignored_dirs:
|
||||
if ignored_dir in real_path.parents:
|
||||
return True
|
||||
return path.relative_path.name in [
|
||||
VERSION_INFO_FILE, '.pytexrc'
|
||||
]
|
||||
|
||||
def build_tex_sources(self) -> bool:
|
||||
self._build_target_type = PyTeXRootDirType.TEX_SOURCE
|
||||
return self._build()
|
||||
|
||||
def build_documentation(self) -> bool:
|
||||
self._build_target_type = PyTeXRootDirType.DOC
|
||||
return self._build()
|
||||
|
||||
def build_tex_files(self) -> bool:
|
||||
self._build_target_type = PyTeXRootDirType.BUILD
|
||||
return self._build()
|
||||
|
||||
def build(self) -> bool:
|
||||
if self._build_target_type is None:
|
||||
raise NotImplementedError
|
||||
return self._build()
|
||||
|
||||
def _update_config_in_input_folder(self):
|
||||
config_file = self.source_root / '.pytexrc' # TODO
|
||||
if config_file.exists():
|
||||
config = PyTeXConfig.from_yaml(config_file)
|
||||
self._pytex_config = config.merge_with(self.pytex_config)
|
||||
|
||||
@property
|
||||
def old_version_info(self) -> VersionInfo:
|
||||
if self._old_version_info is None:
|
||||
self._old_version_info = self._get_version_info()
|
||||
return self._old_version_info
|
||||
|
||||
def _get_version_info(self) -> VersionInfo:
|
||||
version_info_file = self.target_root / VERSION_INFO_FILE
|
||||
if version_info_file.exists():
|
||||
return VersionInfo.from_json(
|
||||
self.target_root / VERSION_INFO_FILE
|
||||
)
|
||||
else:
|
||||
return VersionInfo()
|
||||
|
||||
@property
|
||||
def pytex_config(self) -> PyTeXConfig:
|
||||
if self._pytex_config is None:
|
||||
return PyTeXConfig()
|
||||
else:
|
||||
return self._pytex_config
|
||||
|
||||
@property
|
||||
def target_root(self) -> Path:
|
||||
if self._build_target_type is None:
|
||||
raise NotImplementedError
|
||||
return {
|
||||
PyTeXRootDirType.BUILD: self.pytex_config.build_dir_specification.build_root,
|
||||
PyTeXRootDirType.DOC: self.pytex_config.build_dir_specification.doc_root,
|
||||
PyTeXRootDirType.TEX_SOURCE: self.pytex_config.build_dir_specification.tex_source_root,
|
||||
}[self._build_target_type]
|
||||
|
||||
@property
|
||||
def source_root(self) -> Path:
|
||||
switcher = {
|
||||
PyTeXRootDirType.BUILD: self.pytex_config.build_dir_specification.tex_source_root,
|
||||
PyTeXRootDirType.DOC: self.pytex_config.build_dir_specification.tex_source_root,
|
||||
PyTeXRootDirType.TEX_SOURCE: self.pytex_config.build_dir_specification.pytex_source_root,
|
||||
}
|
||||
return switcher[self._build_target_type]
|
||||
|
||||
def _get_git_version_info(self):
|
||||
pass
|
||||
|
||||
def _parse_config_file(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def supported_extensions(cls) -> List[str]:
|
||||
return [
|
||||
'.sty.pytex',
|
||||
'.cls.pytex',
|
||||
'.dtx.pytex',
|
||||
'.dict.pytex',
|
||||
'.tex.pytex',
|
||||
'.sty',
|
||||
'.cls',
|
||||
'.dtx',
|
||||
'.dict',
|
||||
'.tex',
|
||||
]
|
||||
|
||||
def is_supported_file(self, filename: str) -> bool:
|
||||
return True in [
|
||||
filename.endswith(extension)
|
||||
for extension in self.supported_extensions()
|
||||
]
|
||||
|
||||
def _load_pytex_files(self):
|
||||
self._pytex_files = []
|
||||
if self.pytex_config.recursive:
|
||||
files = self.source_root.rglob('*')
|
||||
else:
|
||||
files = self.source_root.glob('*')
|
||||
for file in files:
|
||||
if self.is_supported_file(file.name):
|
||||
config = self.pytex_config.sub_config(
|
||||
file.name.split('.', 1)[0]
|
||||
)
|
||||
config.merge_with(
|
||||
self.pytex_config.default_formatting_config,
|
||||
strict=False
|
||||
)
|
||||
self._pytex_files.append(
|
||||
PyTeXSourceFile(
|
||||
relative_path=RelativePath(
|
||||
self.source_root,
|
||||
file
|
||||
),
|
||||
default_config=config,
|
||||
git_version_info=self._git_version_info,
|
||||
target=self._build_target_type.to_target()
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: give pytex source file some additional building information
|
||||
|
||||
def _old_version_lookup(self, relative_path: RelativePath) -> FileVersionInfo:
|
||||
matches = [
|
||||
file_version_info
|
||||
for file_version_info in self.old_version_info.files
|
||||
if file_version_info.relative_name == str(relative_path.relative_path)
|
||||
]
|
||||
if len(matches) >= 2:
|
||||
raise NotImplementedError
|
||||
elif len(matches) == 1:
|
||||
return matches[0]
|
||||
else:
|
||||
return FileVersionInfo()
|
||||
|
||||
def _pytex_file_lookup(self, name: str) -> PyTeXSourceFile:
|
||||
matches = [
|
||||
source_file
|
||||
for source_file in self._pytex_files
|
||||
if source_file.relative_path.path.name == name
|
||||
]
|
||||
if len(matches) >= 2:
|
||||
raise NotImplementedError
|
||||
elif len(matches) == 1:
|
||||
return matches[0]
|
||||
else:
|
||||
raise NotImplementedError # what to do in this case? dependency does not exist...
|
||||
|
||||
def _check_output_directory_integrity(self):
|
||||
out_dir_files: List[RelativePath] = [
|
||||
RelativePath(self.target_root, file)
|
||||
for file in self.target_root.rglob('*')
|
||||
]
|
||||
for file in out_dir_files:
|
||||
if not file.is_dir():
|
||||
version = self._old_version_lookup(file)
|
||||
if version.file_hash != md5(file.path):
|
||||
if self.pytex_config.clean_old_files:
|
||||
if not self.is_ignored_in_build_dir(file):
|
||||
raise NotImplementedError # Not ok
|
||||
else:
|
||||
if self.pytex_config.overwrite_existing_files:
|
||||
self._files_to_overwrite.add(file)
|
||||
else:
|
||||
if file.relative_path in \
|
||||
{x.relative_path.relative_path for x in self._files_to_build}:
|
||||
raise NotImplementedError
|
||||
# Not ok iff we are going to write this file
|
||||
|
||||
def _dependencies_hash(self, file: Union[PyTeXOutputFile, PyTeXSourceFile]) -> str:
|
||||
if isinstance(file, PyTeXOutputFile):
|
||||
deps: Set[str] = set(file.dependencies)
|
||||
deps.add(file.source_file.relative_path.path.name)
|
||||
else:
|
||||
deps = set(file.formatter.dependencies)
|
||||
deps.add(file.relative_path.path.name)
|
||||
hashes = set()
|
||||
for dep in deps:
|
||||
hashes.add(
|
||||
self._pytex_file_lookup(dep).file_hash
|
||||
)
|
||||
return md5(hashes)
|
||||
|
||||
def _init_output_files(self):
|
||||
for source_file in self._pytex_files:
|
||||
for output_file in source_file.output_files:
|
||||
h = output_file.with_root(self.target_root)
|
||||
f = PyTeXOutputFile(
|
||||
output_file=h,
|
||||
source_file=source_file,
|
||||
last_version_info=self._old_version_lookup(output_file)
|
||||
)
|
||||
self._output_files.append(
|
||||
f
|
||||
)
|
||||
|
||||
def _compute_files_to_build(self):
|
||||
self._new_version_info.files = []
|
||||
for output_file in self._output_files:
|
||||
if self._dependencies_hash(output_file) != output_file.last_version_info.sources_hash \
|
||||
or output_file.last_version_info.file_hash != output_file.file_hash:
|
||||
self._files_to_build.add(output_file.source_file)
|
||||
else:
|
||||
self._new_version_info.files.append(output_file.last_version_info) # File will not change
|
||||
# TODO actually, this is not totally correct
|
||||
|
||||
def _build_files(self):
|
||||
self._new_config.sub_configs = {}
|
||||
for source_file in self._files_to_build:
|
||||
out_dir = self._tmp_dir / source_file.file_hash
|
||||
out_dir.mkdir(exist_ok=False, parents=True)
|
||||
new_config: List[Tuple[RelativePath, FormattingConfig]] = \
|
||||
source_file.format(self._tmp_dir / source_file.file_hash)
|
||||
for rel_path, config in new_config:
|
||||
self._new_config.sub_configs[
|
||||
rel_path.name
|
||||
] = config
|
||||
for filename in source_file.output_files:
|
||||
logger.verbose(f"[Built] {self.target_root / filename.relative_path}")
|
||||
for output_file in source_file.output_files:
|
||||
# TODO: handle this new config file
|
||||
# TODO: handle git stuff / meta info stuff
|
||||
file_version_info = FileVersionInfo()
|
||||
file_version_info.relative_name = str(output_file.relative_path)
|
||||
file_version_info.file_hash = str(md5(
|
||||
self._tmp_dir / source_file.file_hash / output_file.relative_path.name
|
||||
))
|
||||
file_version_info.sources_hash = self._dependencies_hash(source_file)
|
||||
self._new_version_info.files.append(
|
||||
file_version_info
|
||||
)
|
||||
file_version_info.git_version_info = source_file.formatter.git_version_info # TODO:
|
||||
# only pytex formatters
|
||||
|
||||
def _dump_new_config(self):
|
||||
self.target_root.mkdir(exist_ok=True, parents=True)
|
||||
self._new_config.dump_as_yaml(
|
||||
self.target_root / '.pytexrc'
|
||||
)
|
||||
|
||||
def _move_files(self):
|
||||
for source_file in self._files_to_build:
|
||||
tmp_dir = self._tmp_dir / source_file.file_hash
|
||||
for filename in source_file.output_files:
|
||||
out_dir = self.target_root / source_file.relative_path.relative_path.parent
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(
|
||||
tmp_dir / filename.relative_path.name,
|
||||
out_dir / filename.relative_path.name
|
||||
)
|
||||
if self._tmp_dir.exists():
|
||||
shutil.rmtree(self._tmp_dir)
|
||||
|
||||
def _write_version_info(self):
|
||||
self._new_version_info.dump_as_json(self.target_root / VERSION_INFO_FILE)
|
||||
|
||||
def _build(self) -> bool:
|
||||
self._update_config_in_input_folder()
|
||||
logger.info("Starting build of {}".format(
|
||||
self._build_target_type.value
|
||||
))
|
||||
self._load_pytex_files() # 8ms
|
||||
logger.verbose(f"Found {len(self._pytex_files)} source files")
|
||||
self._init_output_files() # 1ms
|
||||
logger.verbose(f"Found {len(self._output_files)} potential output files to build.")
|
||||
self._compute_files_to_build() # 1ms
|
||||
if len(self._files_to_build) == 0:
|
||||
logger.info(f"Everything up to date, nothing to build!")
|
||||
return True
|
||||
logger.verbose(f"Needing to build {len(self._files_to_build)} many source files.")
|
||||
self._check_output_directory_integrity() # 1ms
|
||||
logger.verbose(f"Starting build")
|
||||
try:
|
||||
self._build_files() # 53 ms
|
||||
except PyTeXError as e:
|
||||
e.add_explanation('while building output files')
|
||||
raise e
|
||||
logger.info(f"Built files")
|
||||
self._dump_new_config()
|
||||
self._move_files()
|
||||
self._write_version_info()
|
||||
return True
|
16
PyTeX/build/build/constants.py
Normal file
16
PyTeX/build/build/constants.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
YAML_BUILD_ROOT = 'build'
|
||||
YAML_PYTEX_SOURCE_ROOT = 'source'
|
||||
YAML_TEX_SOURCE_ROOT = 'tex'
|
||||
YAML_DOC_ROOT = 'doc'
|
||||
YAML_RECURSIVE = 'recursive'
|
||||
YAML_CLEAN_OLD_FILES = 'clean'
|
||||
YAML_OVERWRITE_FILES = 'overwrite'
|
||||
YAML_ALLOW_DIRTY_BUILD = 'dirty'
|
||||
YAML_FORCE_MODE = 'force'
|
||||
YAML_WRAPPER_DIR = 'wrapper'
|
||||
YAML_BUILD = 'build'
|
||||
YAML_DEFAULT = 'default'
|
||||
YAML_CONFIGS = 'configs'
|
||||
YAML_DIRS = 'dirs'
|
||||
|
||||
VERSION_INFO_FILE = 'version_info.json'
|
20
PyTeX/build/build/conversions.py
Normal file
20
PyTeX/build/build/conversions.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from PyTeX.build.build.enums import PyTeXRootDirType
|
||||
from PyTeX.build.build.enums import PyTeXFileType
|
||||
|
||||
|
||||
def pytex_file_type2pytex_root_dir(pytex_file_type: PyTeXFileType) -> PyTeXRootDirType:
|
||||
return {
|
||||
PyTeXFileType.PyTeXSourceFile: PyTeXRootDirType.PYTEX_SOURCE,
|
||||
PyTeXFileType.TeXOutputFile: PyTeXRootDirType.BUILD,
|
||||
PyTeXFileType.TeXDocumentationFile: PyTeXRootDirType.DOC,
|
||||
PyTeXFileType.TeXSourceFile: PyTeXRootDirType.TEX_SOURCE
|
||||
}[pytex_file_type]
|
||||
|
||||
|
||||
def pytex_root_dir2pytex_file_type(pytex_root_dir: PyTeXRootDirType) -> PyTeXFileType:
|
||||
return {
|
||||
PyTeXRootDirType.PYTEX_SOURCE: PyTeXFileType.PyTeXSourceFile,
|
||||
PyTeXRootDirType.BUILD: PyTeXFileType.TeXOutputFile,
|
||||
PyTeXRootDirType.DOC: PyTeXFileType.TeXDocumentationFile,
|
||||
PyTeXRootDirType.TEX_SOURCE: PyTeXFileType.TeXSourceFile,
|
||||
}[pytex_root_dir]
|
24
PyTeX/build/build/enums.py
Normal file
24
PyTeX/build/build/enums.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from enum import Enum
|
||||
|
||||
from PyTeX.format.enums import Target
|
||||
|
||||
|
||||
class PyTeXRootDirType(Enum):
|
||||
BUILD = 'tex distribution'
|
||||
PYTEX_SOURCE = 'pytex sources'
|
||||
DOC = 'documentation'
|
||||
TEX_SOURCE = 'tex sources'
|
||||
|
||||
def to_target(self) -> Target:
|
||||
return {
|
||||
PyTeXRootDirType.TEX_SOURCE: Target.tex_source,
|
||||
PyTeXRootDirType.DOC: Target.documentation,
|
||||
PyTeXRootDirType.BUILD: Target.tex
|
||||
}[self]
|
||||
|
||||
|
||||
class PyTeXFileType(Enum):
|
||||
PyTeXSourceFile = 'PyTeXSourceFile'
|
||||
TeXSourceFile = 'TeXSourceFile'
|
||||
TeXOutputFile = 'TeXOutputFile'
|
||||
TeXDocumentationFile = 'TeXDocumentationFile'
|
22
PyTeX/build/build/hashing.py
Normal file
22
PyTeX/build/build/hashing.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import hashlib
|
||||
from typing import Union, List, Set
|
||||
from pathlib import Path
|
||||
|
||||
# https://stackoverflow.com/a/3431838/16371376
|
||||
|
||||
|
||||
def md5(data: Union[Path, List[str], Set[str]]):
|
||||
hash_md5 = hashlib.md5()
|
||||
if isinstance(data, Path):
|
||||
file: Path = data
|
||||
with open(file, "rb") as f:
|
||||
for block in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(block)
|
||||
else:
|
||||
if isinstance(data, list):
|
||||
set_data: Set[str] = set(data)
|
||||
else:
|
||||
set_data = data
|
||||
hash_md5.update(str(set_data).encode('UTF-8'))
|
||||
return hash_md5.hexdigest()
|
||||
|
121
PyTeX/build/build/pytex_config.py
Normal file
121
PyTeX/build/build/pytex_config.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
from typing import Optional, Dict, List
|
||||
|
||||
from PyTeX.build.build import BuildDirConfig
|
||||
from PyTeX.format.formatting_config import FormattingConfig
|
||||
from .constants import *
|
||||
from ...format.config import Config, clean_dict
|
||||
|
||||
|
||||
class PyTeXConfig(Config):
|
||||
def __init__(
|
||||
self,
|
||||
build_dir_spec: Optional[BuildDirConfig] = None
|
||||
):
|
||||
self._build_dir_specification: Optional[BuildDirConfig] = build_dir_spec
|
||||
|
||||
self._default_formatting_config: Optional[FormattingConfig] = None
|
||||
|
||||
self._recursive: Optional[bool] = None
|
||||
self._overwrite_existing_files: Optional[bool] = None
|
||||
self._clean_old_files: Optional[bool] = None
|
||||
self._allow_dirty: Optional[bool] = None
|
||||
|
||||
self._force_mode: Optional[bool] = None
|
||||
|
||||
self._configs: Optional[Dict[str, FormattingConfig]] = None
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
YAML_BUILD_ROOT: {
|
||||
YAML_RECURSIVE: self._recursive,
|
||||
YAML_OVERWRITE_FILES: self._overwrite_existing_files,
|
||||
YAML_CLEAN_OLD_FILES: self._clean_old_files,
|
||||
YAML_ALLOW_DIRTY_BUILD: self._allow_dirty,
|
||||
YAML_FORCE_MODE: self._force_mode,
|
||||
YAML_DIRS: self.build_dir_specification.to_json()
|
||||
},
|
||||
YAML_DEFAULT: self.default_formatting_config.to_json(),
|
||||
YAML_CONFIGS: {
|
||||
filename: config.to_json()
|
||||
for filename, config in self._configs.items()
|
||||
} if self._configs else None
|
||||
}
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content = self._fill_keys(content)
|
||||
|
||||
build = filled_content[YAML_BUILD]
|
||||
cleaned_dirs = clean_dict(build[YAML_DIRS])
|
||||
self._build_dir_specification = BuildDirConfig.from_json(cleaned_dirs) if cleaned_dirs else None
|
||||
self._recursive = build[YAML_RECURSIVE]
|
||||
self._clean_old_files = build[YAML_CLEAN_OLD_FILES]
|
||||
self._overwrite_existing_files = build[YAML_OVERWRITE_FILES]
|
||||
self._allow_dirty = build[YAML_ALLOW_DIRTY_BUILD]
|
||||
self._force_mode = build[YAML_FORCE_MODE]
|
||||
|
||||
self._default_formatting_config = FormattingConfig.from_json(
|
||||
filled_content[YAML_DEFAULT]
|
||||
)
|
||||
|
||||
self._configs = filled_content[YAML_CONFIGS]
|
||||
|
||||
@property
|
||||
def build_dir_specification(self):
|
||||
if self._build_dir_specification is None:
|
||||
return BuildDirConfig()
|
||||
else:
|
||||
return self._build_dir_specification
|
||||
|
||||
@property
|
||||
def recursive(self) -> bool:
|
||||
if self._recursive is None:
|
||||
return True
|
||||
else:
|
||||
return self._recursive
|
||||
|
||||
@property
|
||||
def overwrite_existing_files(self) -> bool:
|
||||
if self._overwrite_existing_files is None:
|
||||
return False
|
||||
else:
|
||||
return self._overwrite_existing_files
|
||||
|
||||
@property
|
||||
def clean_old_files(self) -> bool:
|
||||
if self._clean_old_files is None:
|
||||
return False
|
||||
else:
|
||||
return self._clean_old_files
|
||||
|
||||
@property
|
||||
def allow_dirty(self) -> bool:
|
||||
if self._allow_dirty is None:
|
||||
return False
|
||||
else:
|
||||
return self._allow_dirty
|
||||
|
||||
@property
|
||||
def default_formatting_config(self) -> FormattingConfig:
|
||||
if self._default_formatting_config is None:
|
||||
return FormattingConfig()
|
||||
else:
|
||||
return self._default_formatting_config
|
||||
|
||||
@property
|
||||
def sub_configs(self) -> Dict:
|
||||
if self._configs is None:
|
||||
return {}
|
||||
else:
|
||||
return self._configs
|
||||
|
||||
def sub_config(self, name: str) -> FormattingConfig:
|
||||
if name in self.sub_configs.keys():
|
||||
return FormattingConfig.from_json(self.sub_configs[name])
|
||||
else:
|
||||
return FormattingConfig()
|
||||
|
||||
@sub_configs.setter
|
||||
def sub_configs(self, configs: List[FormattingConfig]):
|
||||
self._configs = configs
|
||||
|
||||
|
91
PyTeX/build/build/pytex_file.py
Normal file
91
PyTeX/build/build/pytex_file.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Tuple, Union
|
||||
|
||||
from .relative_path import RelativePath
|
||||
from PyTeX.format.formatterif import FormatterIF
|
||||
from PyTeX.build.build.enums import PyTeXFileType
|
||||
from .hashing import md5
|
||||
from ...exceptions import PyTeXException
|
||||
from ...format.enums import Target
|
||||
from ...format.errors import PyTeXError
|
||||
from ...format.formatting_config import FormattingConfig
|
||||
from ...format.auto_format import formatter_from_file_extension
|
||||
from ...format.git_version_info import GitVersionInfo
|
||||
|
||||
|
||||
class PyTeXSourceFile:
|
||||
def __init__(
|
||||
self,
|
||||
relative_path: RelativePath,
|
||||
formatter: Optional[FormatterIF] = None,
|
||||
default_config: Optional[FormattingConfig] = None,
|
||||
git_version_info: Optional[GitVersionInfo] = None,
|
||||
pytex_file_type: Optional[PyTeXFileType] = None,
|
||||
target: Optional[Target] = None
|
||||
):
|
||||
self._relative_path: RelativePath = relative_path
|
||||
if formatter is not None:
|
||||
self._formatter: FormatterIF = formatter
|
||||
else:
|
||||
self._formatter = formatter_from_file_extension(
|
||||
relative_path.path,
|
||||
config=default_config,
|
||||
git_version_info=git_version_info,
|
||||
locate_file_config=True,
|
||||
allow_infile_config=True,
|
||||
target=target
|
||||
)
|
||||
self._pytex_file_type: Optional[PyTeXFileType] = pytex_file_type
|
||||
self._file_hash: Optional[str] = None
|
||||
|
||||
@property
|
||||
def file_hash(self) -> str:
|
||||
if self._file_hash is None:
|
||||
self.update_file_hash()
|
||||
return self._file_hash
|
||||
|
||||
def update_file_hash(self):
|
||||
self._file_hash = md5(self._relative_path.path)
|
||||
|
||||
@property
|
||||
def relative_path(self) -> RelativePath:
|
||||
return self._relative_path
|
||||
|
||||
@property
|
||||
def pytex_file_type(self) -> Optional[PyTeXFileType]:
|
||||
return self._pytex_file_type
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[RelativePath]:
|
||||
if self._formatter is None:
|
||||
raise NotImplementedError # TODO
|
||||
files: List[str] = self._formatter.output_files
|
||||
paths = [
|
||||
RelativePath(self._relative_path.root_dir, self._relative_path.with_name(filename))
|
||||
for filename in files
|
||||
]
|
||||
return paths
|
||||
|
||||
@property
|
||||
def formatter(self) -> Optional[FormatterIF]:
|
||||
return self._formatter
|
||||
|
||||
@formatter.setter
|
||||
def formatter(self, formatter):
|
||||
self._formatter = formatter
|
||||
|
||||
def format(self, target_root: Union[Path, RelativePath]) -> List[Tuple[RelativePath, FormattingConfig]]:
|
||||
if self._formatter is None:
|
||||
raise NotImplementedError # TODO
|
||||
try:
|
||||
configs = self._formatter.format(
|
||||
target_root.path if isinstance(target_root, RelativePath) else target_root
|
||||
)
|
||||
except PyTeXError as e:
|
||||
e.add_explanation(f'while processing {str(self.relative_path.path)}')
|
||||
raise e
|
||||
rel_configs = [
|
||||
(self._relative_path.with_name(filename), config)
|
||||
for [filename, config] in configs
|
||||
]
|
||||
return rel_configs
|
34
PyTeX/build/build/pytex_output_file.py
Normal file
34
PyTeX/build/build/pytex_output_file.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional, Union, List, Tuple
|
||||
|
||||
from .enums import PyTeXRootDirType
|
||||
from .pytex_config import PyTeXConfig
|
||||
from ...logger import logger
|
||||
from .constants import *
|
||||
from ..versioning.version_info.version_info import VersionInfo, FileVersionInfo
|
||||
from .pytex_file import PyTeXSourceFile
|
||||
from .relative_path import RelativePath
|
||||
from .hashing import md5
|
||||
|
||||
|
||||
class PyTeXOutputFile:
|
||||
def __init__(
|
||||
self,
|
||||
source_file: PyTeXSourceFile,
|
||||
output_file: RelativePath,
|
||||
last_version_info: FileVersionInfo
|
||||
):
|
||||
self.output_file: RelativePath = output_file
|
||||
self.source_file: PyTeXSourceFile = source_file
|
||||
self.last_version_info: FileVersionInfo = last_version_info
|
||||
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return self.source_file.formatter.dependencies
|
||||
|
||||
def is_recent(self) -> bool:
|
||||
return self.last_version_info.file_hash == self.source_file.file_hash
|
||||
|
||||
@property
|
||||
def file_hash(self) -> str:
|
||||
return md5(self.output_file.path)
|
69
PyTeX/build/build/relative_path.py
Normal file
69
PyTeX/build/build/relative_path.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from ctypes import Union
|
||||
from pathlib import Path
|
||||
|
||||
from PyTeX.build.build.enums import PyTeXRootDirType
|
||||
|
||||
|
||||
class RelativePath:
|
||||
"""
|
||||
Represents a path that knows of its corresponding root directory
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_dir: Path,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
pass
|
||||
self._path = Path(*args, **kwargs)
|
||||
self._root_dir = root_dir
|
||||
|
||||
def __getattr__(self, attr):
|
||||
ret = getattr(self._path, attr)
|
||||
if isinstance(ret, Path):
|
||||
return RelativePath(self._pytex_root_dir_type, ret)
|
||||
else:
|
||||
return ret
|
||||
|
||||
def __truediv__(self, other):
|
||||
if isinstance(other, RelativePath):
|
||||
path = self._path.__truediv__(other.path)
|
||||
else:
|
||||
path = self._path.__truediv__(other)
|
||||
return RelativePath(self._root_dir, path)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
if isinstance(other, RelativePath):
|
||||
path = self._path.__rtruediv__(other.path)
|
||||
else:
|
||||
path = self._path.__rtruediv__(other)
|
||||
return RelativePath(self._root_dir, path)
|
||||
|
||||
def __str__(self):
|
||||
return self._path.__str__()
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def root_dir_type(self) -> PyTeXRootDirType:
|
||||
return self._pytex_root_dir_type
|
||||
|
||||
@property
|
||||
def root_dir(self) -> Path:
|
||||
return self._root_dir
|
||||
|
||||
@property
|
||||
def relative_path(self) -> Path:
|
||||
try:
|
||||
return self.relative_to(self._root_dir)
|
||||
except ValueError as e:
|
||||
raise NotImplementedError
|
||||
|
||||
def with_root(self, root: Path):
|
||||
return RelativePath(
|
||||
root,
|
||||
root / self.relative_path
|
||||
)
|
0
PyTeX/build/versioning/__init__.py
Normal file
0
PyTeX/build/versioning/__init__.py
Normal file
0
PyTeX/build/versioning/git/__init__.py
Normal file
0
PyTeX/build/versioning/git/__init__.py
Normal file
27
PyTeX/build/versioning/git/get_version_info.py
Normal file
27
PyTeX/build/versioning/git/get_version_info.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
import git
|
||||
from PyTeX.format.repo_status_info import RepoStatusInfo
|
||||
from .recent import get_latest_commit
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_status_info_from_file(file: Path) -> RepoStatusInfo:
|
||||
try:
|
||||
repo = git.Repo(file, search_parent_directories=True)
|
||||
except git.InvalidGitRepositoryError:
|
||||
return RepoStatusInfo()
|
||||
return get_repo_status_info(repo)
|
||||
|
||||
|
||||
def get_repo_status_info(repo: git.Repo) -> RepoStatusInfo:
|
||||
info = RepoStatusInfo()
|
||||
try:
|
||||
info.branch = str(repo.active_branch)
|
||||
except TypeError: # No branch available
|
||||
pass
|
||||
try:
|
||||
info.version = repo.git.describe()
|
||||
except git.GitCommandError: # No tags available to describe commit
|
||||
pass
|
||||
info.commit_hash = get_latest_commit(repo).hexsha
|
||||
info.dirty = repo.is_dirty(untracked_files=True)
|
||||
return info
|
|
@ -1,11 +1,18 @@
|
|||
from pathlib import Path
|
||||
from typing import Union, Optional, List
|
||||
|
||||
import git
|
||||
|
||||
from .git_version import get_latest_commit
|
||||
from typing import Union, Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
def get_latest_commit(repo) -> git.Commit:
|
||||
if repo.head.is_detached:
|
||||
return repo.head.commit
|
||||
else:
|
||||
return repo.head.ref.commit
|
||||
|
||||
|
||||
def is_recent(file: Path, repo: git.Repo, compare: Optional[Union[git.Commit, List[git.Commit]]] = None) -> Optional[bool]:
|
||||
def is_recent(file: Path, repo: git.Repo, compare: Optional[Union[git.Commit, List[git.Commit]]] = None) -> Optional[
|
||||
bool]:
|
||||
"""
|
||||
:param file: file to check
|
||||
:param repo: repo that the file belongs to
|
0
PyTeX/build/versioning/version_info/__init__.py
Normal file
0
PyTeX/build/versioning/version_info/__init__.py
Normal file
26
PyTeX/build/versioning/version_info/constants.py
Normal file
26
PyTeX/build/versioning/version_info/constants.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
JSON_NAME = 'name'
|
||||
JSON_BUILD_TIME = 'build time'
|
||||
JSON_SOURCE_FILES = 'source files'
|
||||
|
||||
JSON_MD5_CHECKSUM = 'md5 checksum'
|
||||
JSON_PYTEX = 'pytex'
|
||||
JSON_REPOSITORY = 'repository'
|
||||
|
||||
JSON_VERSION = 'source version'
|
||||
JSON_COMMIT_HASH = 'commit hash'
|
||||
JSON_BRANCH = 'branch'
|
||||
JSON_DIRTY = 'dirty'
|
||||
|
||||
DEFAULT_VERSION = '0.0.0'
|
||||
DEFAULT_BRANCH = 'NO-BRANCH'
|
||||
DEFAULT_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
JSON_FILE_HASH = 'file_hash'
|
||||
JSON_SOURCES_HASH = 'sources_hash'
|
||||
JSON_RELATIVE_NAME = 'relative_name'
|
||||
JSON_GIT_VERSION_INFO = 'git_version_info'
|
||||
|
||||
NO_RELATIVE_NAME = 'NO_NAME'
|
||||
NO_BUILD_TIME = 'no_build_time'
|
||||
|
||||
JSON_FILE_VERSIONS = 'file_versions'
|
136
PyTeX/build/versioning/version_info/version_info.py
Normal file
136
PyTeX/build/versioning/version_info/version_info.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
from typing import Optional, List, Dict
|
||||
|
||||
from .constants import *
|
||||
from ....format.config import Config
|
||||
from ....format.git_version_info import GitVersionInfo
|
||||
from ...build.enums import *
|
||||
|
||||
|
||||
class FileVersionInfo(Config):
|
||||
def __init__(self):
|
||||
self._relative_name: Optional[str] = None
|
||||
self._file_hash: Optional[str] = None
|
||||
self._sources_hash: Optional[str] = None
|
||||
|
||||
# Meta properties actually not needed for build itself
|
||||
self._git_version_info: Optional[GitVersionInfo] = None
|
||||
self._build_time: Optional[str] = None
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content = self._fill_keys(content)
|
||||
self._relative_name = filled_content[JSON_RELATIVE_NAME]
|
||||
self._file_hash = filled_content[JSON_FILE_HASH]
|
||||
self._sources_hash = filled_content[JSON_SOURCES_HASH]
|
||||
self._build_time = filled_content[JSON_BUILD_TIME]
|
||||
self._git_version_info = GitVersionInfo.from_json(
|
||||
filled_content[JSON_GIT_VERSION_INFO]
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
JSON_RELATIVE_NAME: self._relative_name,
|
||||
JSON_FILE_HASH: self._file_hash,
|
||||
JSON_SOURCES_HASH: self._sources_hash,
|
||||
JSON_BUILD_TIME: self._build_time,
|
||||
JSON_GIT_VERSION_INFO: self.git_version_info.to_json()
|
||||
}
|
||||
|
||||
@property
|
||||
def file_hash(self) -> str:
|
||||
if self._file_hash is None:
|
||||
return DEFAULT_HASH
|
||||
else:
|
||||
return self._file_hash
|
||||
|
||||
@file_hash.setter
|
||||
def file_hash(self, file_hash: str):
|
||||
self._file_hash = file_hash
|
||||
|
||||
@property
|
||||
def sources_hash(self) -> str:
|
||||
if self._sources_hash is None:
|
||||
return DEFAULT_HASH
|
||||
else:
|
||||
return self._sources_hash
|
||||
|
||||
@sources_hash.setter
|
||||
def sources_hash(self, sources_hash: str):
|
||||
self._sources_hash = sources_hash
|
||||
|
||||
@property
|
||||
def relative_name(self) -> str:
|
||||
if self._relative_name is None:
|
||||
return NO_RELATIVE_NAME
|
||||
else:
|
||||
return self._relative_name
|
||||
|
||||
@relative_name.setter
|
||||
def relative_name(self, relative_name: str):
|
||||
self._relative_name = relative_name
|
||||
|
||||
@property
|
||||
def git_version_info(self) -> GitVersionInfo:
|
||||
if self._git_version_info is None:
|
||||
return GitVersionInfo()
|
||||
else:
|
||||
return self._git_version_info
|
||||
|
||||
@git_version_info.setter
|
||||
def git_version_info(self, git_version_info: GitVersionInfo):
|
||||
self._git_version_info = git_version_info
|
||||
|
||||
@property
|
||||
def build_time(self) -> str:
|
||||
if self._build_time is None:
|
||||
return NO_BUILD_TIME
|
||||
else:
|
||||
return self._build_time
|
||||
|
||||
@build_time.setter
|
||||
def build_time(self, build_time: str):
|
||||
self._build_time = build_time
|
||||
|
||||
|
||||
class VersionInfo(Config):
|
||||
def __init__(self):
|
||||
self._pytex_dir_type: Optional[PyTeXRootDirType] = None
|
||||
self._files: Optional[List[FileVersionInfo]] = None
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content: Dict = self._fill_keys(content)
|
||||
self._pytex_dir_type = None # TODO
|
||||
self._files = [
|
||||
FileVersionInfo.from_json(entry)
|
||||
for entry in filled_content[JSON_FILE_VERSIONS]
|
||||
]
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
JSON_FILE_VERSIONS: [
|
||||
file_version_info.to_json()
|
||||
for file_version_info in self.files
|
||||
]
|
||||
}
|
||||
|
||||
@property
|
||||
def pytex_dir_type(self) -> PyTeXRootDirType:
|
||||
if self._pytex_dir_type is None:
|
||||
return PyTeXRootDirType.PYTEX_SOURCE
|
||||
else:
|
||||
return self._pytex_dir_type
|
||||
|
||||
@pytex_dir_type.setter
|
||||
def pytex_dir_type(self, pytex_dir_type: PyTeXRootDirType):
|
||||
self._pytex_dir_type = pytex_dir_type
|
||||
|
||||
@property
|
||||
def files(self) -> List[FileVersionInfo]:
|
||||
if self._files is None:
|
||||
return []
|
||||
else:
|
||||
return self._files
|
||||
|
||||
@files.setter
|
||||
def files(self, files: List[FileVersionInfo]):
|
||||
self._files = files
|
||||
|
2
PyTeX/exceptions/__init__.py
Normal file
2
PyTeX/exceptions/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class PyTeXException(Exception):
|
||||
pass
|
0
PyTeX/format/__init__.py
Normal file
0
PyTeX/format/__init__.py
Normal file
84
PyTeX/format/auto_format.py
Normal file
84
PyTeX/format/auto_format.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional, Dict, Union, Type
|
||||
|
||||
from .formatting_config import FormattingConfig
|
||||
|
||||
from .dict_formatter import DictFormatter
|
||||
from .simple_tex_formatter import SimpleTeXFormatter
|
||||
from .dtx_formatter import DTXFormatter
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from .git_version_info import GitVersionInfo
|
||||
from .docstrip_formatter import DocStripFormatter
|
||||
from .nothing_formatter import NothingFormatter
|
||||
from .copy_formatter import CopyFormatter
|
||||
from .default_macros import get_default_macros
|
||||
from .enums import TeXType, Target
|
||||
|
||||
|
||||
def formatter_from_file_extension(
|
||||
input_file: Path,
|
||||
config: Optional[FormattingConfig] = None,
|
||||
git_version_info: Optional[GitVersionInfo] = None,
|
||||
locate_file_config: bool = True,
|
||||
allow_infile_config: bool = True,
|
||||
default_macros: bool = True,
|
||||
target: Optional[Target] = None,
|
||||
) -> PyTeXFormatter:
|
||||
|
||||
extension_switcher: Dict[str, TeXType] = {
|
||||
'dtx.pytex': TeXType.TeXDocstrip,
|
||||
'dtx': TeXType.TeXDocstrip,
|
||||
'sty.pytex': TeXType.TeXPackage,
|
||||
'sty': TeXType.TeXPackage,
|
||||
'cls.pytex': TeXType.TeXClass,
|
||||
'cls': TeXType.TeXClass,
|
||||
'dict.pytex': TeXType.TeXDictionary,
|
||||
'dict': TeXType.TeXDictionary,
|
||||
'tex.pytex': TeXType.TeXDocumentation,
|
||||
'tex': TeXType.TeXDocumentation,
|
||||
}
|
||||
source_formatter_switcher: Dict[str, Type[PyTeXFormatter]] = {
|
||||
'dtx.pytex': DTXFormatter,
|
||||
'sty.pytex': SimpleTeXFormatter,
|
||||
'cls.pytex': SimpleTeXFormatter,
|
||||
'dict.pytex': DictFormatter
|
||||
}
|
||||
tex_formatter_switcher = {
|
||||
'ins': NothingFormatter,
|
||||
'drv': NothingFormatter,
|
||||
'dtx': DocStripFormatter,
|
||||
'dict': CopyFormatter,
|
||||
'cls': CopyFormatter,
|
||||
'sty': CopyFormatter,
|
||||
}
|
||||
documentation_formatter_switcher = {
|
||||
|
||||
}
|
||||
|
||||
if target == Target.tex_source:
|
||||
switcher = source_formatter_switcher
|
||||
elif target == Target.tex:
|
||||
switcher = tex_formatter_switcher
|
||||
elif target == Target.documentation:
|
||||
switcher = documentation_formatter_switcher
|
||||
else:
|
||||
switcher = source_formatter_switcher | tex_formatter_switcher # Default case
|
||||
try:
|
||||
[name, extension] = input_file.name.split('.', maxsplit=1)
|
||||
except ValueError:
|
||||
raise NotImplementedError
|
||||
|
||||
config.tex_type = extension_switcher[extension] # This sets the textype from file extension
|
||||
|
||||
formatter = switcher[extension](
|
||||
input_file=input_file,
|
||||
config=config,
|
||||
git_version_info=git_version_info,
|
||||
locate_file_config=locate_file_config,
|
||||
allow_infile_config=allow_infile_config
|
||||
)
|
||||
if default_macros and target == Target.tex_source:
|
||||
formatter.macros = get_default_macros(formatter.config.tex_flavour, formatter.config.tex_type)
|
||||
return formatter
|
||||
|
||||
|
145
PyTeX/format/config.py
Normal file
145
PyTeX/format/config.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Union, List
|
||||
|
||||
import yaml
|
||||
|
||||
def clean_list(list_: List) -> Optional[List]:
|
||||
"""
|
||||
Recursively removes all entries from list that are None,
|
||||
empty dictionaries or empty lists.
|
||||
Dicts ore lists that are None after cleaning will also be removed
|
||||
|
||||
Typically applied before dumping data to JSON where None values are default
|
||||
and will be restored on read-in 'automatically'
|
||||
|
||||
:return:
|
||||
:param list_: Any list
|
||||
:return:
|
||||
"""
|
||||
ret = []
|
||||
for elem in list_:
|
||||
if type(elem) == list:
|
||||
ret.append(clean_list(elem))
|
||||
elif type(elem) == dict:
|
||||
ret.append(clean_dict(elem))
|
||||
elif elem is not None:
|
||||
ret.append(elem)
|
||||
return ret if ret != [] else None
|
||||
|
||||
def clean_dict(dictionary: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
Recursively removes all entries from dictionary that are None,
|
||||
empty dictionaries or empty lists.
|
||||
Keys whose value is a dict / list that only contains non-valued keys will
|
||||
then also be removed.
|
||||
|
||||
Typically applied before dumping data to JSON where None values are default
|
||||
and will be restored on read-in 'automatically'
|
||||
|
||||
:param dictionary: Any dictionary
|
||||
:return:
|
||||
"""
|
||||
aux: Dict = {
|
||||
k: clean_dict(v) for k, v in dictionary.items() if type(v) == dict
|
||||
} | {
|
||||
k: clean_list(v)
|
||||
for k, v in dictionary.items() if type(v) == list
|
||||
} | {
|
||||
k: v for k, v in dictionary.items() if type(v) != dict and type(v) != list
|
||||
}
|
||||
aux2: Dict = {
|
||||
k: v for k, v in aux.items() if v is not None
|
||||
}
|
||||
return aux2 if aux2 != {} and aux2 != [] else None
|
||||
|
||||
|
||||
class Config:
|
||||
def merge_with(self, other: Config, strict: bool = False):
|
||||
"""
|
||||
Merges the other config into this one.
|
||||
In conflicts, the called-on instance takes effect
|
||||
:param other:
|
||||
:param strict: whether conflicting options are allowed or not
|
||||
:return: self
|
||||
"""
|
||||
for var in vars(self):
|
||||
if not getattr(self, var):
|
||||
setattr(self, var, getattr(other, var))
|
||||
else:
|
||||
if strict and getattr(other, var) is not None and getattr(self, var) != getattr(other, var):
|
||||
raise NotImplementedError
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, content: Union[Path, Dict]):
|
||||
if isinstance(content, Path):
|
||||
with open(content, 'r') as file:
|
||||
json_content: Dict = json.load(file)
|
||||
else:
|
||||
json_content = content
|
||||
config = cls()
|
||||
config.set_from_json(json_content)
|
||||
return config
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, content: Union[Path, str]):
|
||||
if isinstance(content, Path):
|
||||
with open(content, 'r') as file:
|
||||
json_content: Dict = yaml.safe_load(file)
|
||||
else:
|
||||
json_content = yaml.safe_load(content)
|
||||
return cls.from_json(json_content)
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def dump_as_yaml(self, filename: Path, clean_none_entries: bool = True):
|
||||
with filename.open('w') as file:
|
||||
if clean_none_entries:
|
||||
simple_dict = clean_dict(self.to_json())
|
||||
else:
|
||||
simple_dict = self.to_json()
|
||||
if simple_dict is not None:
|
||||
yaml.dump(simple_dict, file)
|
||||
else:
|
||||
pass # TODO
|
||||
|
||||
def dump_as_json(self, filename: Path, clean_none_entries: bool = True):
|
||||
with open(filename, 'w') as config:
|
||||
if clean_none_entries:
|
||||
simple_dict = clean_dict(self.to_json())
|
||||
else:
|
||||
simple_dict = self.to_json()
|
||||
if simple_dict is not None:
|
||||
json.dump(simple_dict, config, indent=4)
|
||||
else:
|
||||
pass # TODO
|
||||
|
||||
@classmethod
|
||||
def _fill_keys(cls, dictionary: Optional[Dict]):
|
||||
if dictionary is None:
|
||||
return cls().to_json()
|
||||
else:
|
||||
return recursive_merge_dictionaries(
|
||||
cls().to_json(),
|
||||
dictionary
|
||||
)
|
||||
|
||||
|
||||
def recursive_merge_dictionaries(dict1: Dict, dict2: Dict) -> Dict:
|
||||
aux1 = {
|
||||
k: v for k, v in dict1.items() if type(v) == dict
|
||||
}
|
||||
aux2 = {
|
||||
k: v for k, v in dict2.items() if type(v) == dict
|
||||
}
|
||||
merged = {
|
||||
k: recursive_merge_dictionaries(v, aux2[k]) for k, v in aux1.items() if k in aux2.keys()
|
||||
}
|
||||
return dict1 | dict2 | merged
|
69
PyTeX/format/constants.py
Normal file
69
PyTeX/format/constants.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
INFILE_CONFIG_BEGIN_CONFIG = 'config'
|
||||
INFILE_CONFIG_END_CONFIG = 'endconfig'
|
||||
PYTEX_CONFIG_FILE_EXTENSION = '.conf'
|
||||
DICTIONARY_KEY_COLUMN_NAME = 'key'
|
||||
DICTIONARY_NAMING_PATTERN = 'translator-{dict_name}-dictionary-{language}.dict'
|
||||
FORMATTER_PREFIX = '!'
|
||||
IMPLEMENTATION_BEGIN_MACRO = 'beginimpl'
|
||||
IMPLEMENTATION_END_MACRO = 'endimpl'
|
||||
|
||||
YAML_INFO = 'info'
|
||||
YAML_NAMING_SCHEME = 'name'
|
||||
YAML_LICENSE = 'license'
|
||||
YAML_INCLUDE_LICENSE = 'include'
|
||||
YAML_DESCRIPTION = 'description'
|
||||
YAML_EXTRA = 'extra'
|
||||
YAML_HEADER = 'header'
|
||||
YAML_INCLUDE_EXTRA_HEADER = 'include'
|
||||
YAML_INCLUDE_BUILD_TIME = 'time'
|
||||
YAML_INCLUDE_VERSION = 'version'
|
||||
YAML_INCLUDE_INFO_TEXT = 'info'
|
||||
YAML_INCLUDE_TIME = 'time'
|
||||
YAML_AUTHOR = 'author'
|
||||
YAML_VERSION = 'version'
|
||||
YAML_PATH = 'path'
|
||||
YAML_INCLUDE_DRV = 'drv'
|
||||
YAML_INCLUDE_INS = 'ins'
|
||||
YAML_DOCSTRIP_GUARDS = 'guards'
|
||||
YAML_DEPENDENCIES = 'dependencies'
|
||||
YAML_DOC_DEPENDENCIES = 'doc'
|
||||
YAML_TEX_DEPENDENCIES = 'tex'
|
||||
YAML_TEX_FLAVOUR = 'flavour'
|
||||
YAML_TEX_TYPE = 'type'
|
||||
YAML_TEX_OUT_TYPE = 'outtype'
|
||||
YAML_TEXT = 'text'
|
||||
YAML_REPO = 'repo'
|
||||
YAML_PYTEX = 'pytex'
|
||||
YAML_DOCSTRIP = 'docstrip'
|
||||
|
||||
|
||||
INS_FILE = [
|
||||
r'\begingroup',
|
||||
r'\input docstrip.tex',
|
||||
r'\keepsilent',
|
||||
r'\preamble',
|
||||
r'___________________________________________________________',
|
||||
r'{preamble}',
|
||||
r'',
|
||||
r'\endpreamble',
|
||||
r'\postamble',
|
||||
r'',
|
||||
r'{postamble}',
|
||||
r'',
|
||||
r'\endpostamble',
|
||||
r'\askforoverwritefalse',
|
||||
r'',
|
||||
r'\generate{{\file{{{outfile}}}{{\from{{{infile}}}{{{guards}}}}}}}',
|
||||
r'',
|
||||
r'\def\tmpa{{plain}}',
|
||||
r'\ifx\tmpa\fmtname\endgroup\expandafter\bye\fi',
|
||||
r'\endgroup',
|
||||
]
|
||||
|
||||
DRV_FILE = [
|
||||
r'\documentclass{{{documentclass}}}',
|
||||
r'{preamble}',
|
||||
r'\begin{{document}}',
|
||||
r'\DocInput{{{infile}}}',
|
||||
r'\end{{document}}'
|
||||
]
|
21
PyTeX/format/copy_formatter.py
Normal file
21
PyTeX/format/copy_formatter.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from .formatting_config import FormattingConfig
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class CopyFormatter(PyTeXFormatter):
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[str]:
|
||||
return [self.input_file.name]
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
shutil.copy(self.input_file, build_dir / self.input_file.name)
|
||||
return []
|
||||
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # TODO
|
112
PyTeX/format/default_macros.py
Normal file
112
PyTeX/format/default_macros.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
from .macros import *
|
||||
from .enums import TeXFlavour, Argument, TeXType
|
||||
|
||||
|
||||
def make_simple_macro(name: str, arg):
|
||||
return SimpleMacro(
|
||||
name,
|
||||
MacroReplacement(
|
||||
'%s',
|
||||
arg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_default_macros(tex_flavour: TeXFlavour, tex_type: TeXType):
|
||||
both = [
|
||||
make_simple_macro('!', FormatterProperty.file_prefix),
|
||||
make_simple_macro('name', FormatterProperty.name),
|
||||
make_simple_macro('author', FormatterProperty.author),
|
||||
make_simple_macro('date', FormatterProperty.date),
|
||||
make_simple_macro('year', FormatterProperty.year),
|
||||
make_simple_macro('shortauthor', FormatterProperty.shortauthor),
|
||||
make_simple_macro('version', FormatterProperty.version),
|
||||
make_simple_macro('filename', FormatterProperty.file_name),
|
||||
make_simple_macro('prefix', FormatterProperty.file_prefix),
|
||||
make_simple_macro('repoversion', FormatterProperty.repo_version),
|
||||
make_simple_macro('repobranch', FormatterProperty.repo_branch),
|
||||
make_simple_macro('repocommit', FormatterProperty.repo_commit),
|
||||
make_simple_macro('repodirty', FormatterProperty.repo_dirty),
|
||||
make_simple_macro('sourcename', FormatterProperty.source_file_name),
|
||||
ConfigEndMacro(),
|
||||
ConfigBeginMacro(),
|
||||
]
|
||||
docstrip = [
|
||||
make_simple_macro('outtype', FormatterProperty.tex_out_type),
|
||||
MacroCodeBeginMacro(),
|
||||
MacroCodeEndMacro(),
|
||||
GuardMacro(),
|
||||
ImplementationBeginMacro(),
|
||||
ImplementationEndMacro(),
|
||||
]
|
||||
tex2 = [
|
||||
ArgumentMacro(
|
||||
'newif',
|
||||
2,
|
||||
MacroReplacement(
|
||||
r'\newif\if%s@%s\%s@%s%s',
|
||||
FormatterProperty.file_prefix,
|
||||
Argument.one,
|
||||
FormatterProperty.file_prefix,
|
||||
Argument.one,
|
||||
Argument.two
|
||||
)
|
||||
),
|
||||
ArgumentMacro(
|
||||
'setif',
|
||||
2,
|
||||
MacroReplacement(
|
||||
r'\%s@%s%s',
|
||||
FormatterProperty.file_prefix,
|
||||
Argument.one,
|
||||
Argument.two
|
||||
)
|
||||
),
|
||||
ArgumentMacro(
|
||||
'if',
|
||||
1,
|
||||
MacroReplacement(
|
||||
r'\if%s@%s',
|
||||
FormatterProperty.file_prefix,
|
||||
Argument.one
|
||||
)
|
||||
),
|
||||
ArgumentMacro(
|
||||
'header',
|
||||
1,
|
||||
MacroReplacement(
|
||||
r'\Provides%s{%s}[%s - %s (%s)]',
|
||||
FormatterProperty.Tex_type,
|
||||
FormatterProperty.name,
|
||||
FormatterProperty.date,
|
||||
Argument.one,
|
||||
FormatterProperty.version
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
tex3 = [
|
||||
ArgumentMacro(
|
||||
'header',
|
||||
1,
|
||||
MacroReplacement(
|
||||
'\\ProvidesExpl%s { %s } { %s } { %s }\n { %s }',
|
||||
FormatterProperty.Tex_type,
|
||||
FormatterProperty.name,
|
||||
FormatterProperty.date,
|
||||
FormatterProperty.version,
|
||||
FormatterProperty.description
|
||||
)
|
||||
)
|
||||
]
|
||||
macros = both
|
||||
if tex_flavour == TeXFlavour.LaTeX2e:
|
||||
macros += tex2
|
||||
elif tex_flavour == TeXFlavour.LaTeX3:
|
||||
macros += tex3
|
||||
else:
|
||||
raise NotImplementedError
|
||||
if tex_type == TeXType.TeXDocstrip:
|
||||
macros += docstrip
|
||||
|
||||
return macros
|
92
PyTeX/format/dict_formatter.py
Normal file
92
PyTeX/format/dict_formatter.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
import csv
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
from .constants import *
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from ..logger import logger
|
||||
from .formatting_config import FormattingConfig
|
||||
|
||||
|
||||
class DictFormatter(PyTeXFormatter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DictFormatter, self).__init__(*args, **kwargs)
|
||||
with open(self.input_file, 'r') as file:
|
||||
line = file.readline()
|
||||
parts = [entry.strip() for entry in line.split(',')]
|
||||
if not len(parts) >= 1:
|
||||
raise NotImplementedError
|
||||
if not parts[0] == DICTIONARY_KEY_COLUMN_NAME:
|
||||
raise NotImplementedError
|
||||
self._languages = parts[1:]
|
||||
self._dict_name = self.input_file.name.split('.')[0]
|
||||
self._translations = None
|
||||
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # No dependencies for dictionaries
|
||||
|
||||
@property
|
||||
def translations(self) -> Dict:
|
||||
if self._translations is None:
|
||||
self._translations = self.parse()
|
||||
return self._translations
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[str]:
|
||||
return [
|
||||
DICTIONARY_NAMING_PATTERN.format(
|
||||
dict_name=self._dict_name,
|
||||
language=language
|
||||
)
|
||||
for language in self._languages
|
||||
]
|
||||
|
||||
def parse(self) -> Dict:
|
||||
with open(self.input_file, newline='') as csvfile:
|
||||
spamreader = csv.reader(csvfile, delimiter=',', quotechar='|')
|
||||
next(spamreader) # Skip languages line
|
||||
translations: Dict = {}
|
||||
for language in self._languages:
|
||||
translations[language] = {}
|
||||
for line in spamreader:
|
||||
if not len(line) == len(self._languages) + 1:
|
||||
raise NotImplementedError # Invalid file format
|
||||
for n in range(1, len(line)):
|
||||
translations[self._languages[n-1]][line[0]] = line[n]
|
||||
return translations
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.make_header() # TODO: add kwargs
|
||||
for language in self._languages:
|
||||
lines: List[str] = [self.make_header(), '']
|
||||
lines += r'\ProvidesDictionary{{translator-{dict_name}-dictionary}}{{{language}}}'.format(
|
||||
dict_name=self._dict_name,
|
||||
language=language
|
||||
)
|
||||
lines += ['\n'] * 2
|
||||
for key in self.translations[language].keys():
|
||||
if self.translations[language][key].strip() != '':
|
||||
lines += r'\providetranslation{{{key}}}{{{translation}}}'.format(
|
||||
key=key.strip(),
|
||||
translation=self.translations[language][key].strip()
|
||||
)
|
||||
lines += '\n'
|
||||
else:
|
||||
logger.warning(
|
||||
'Empty translation key found in {filename}'.format(
|
||||
filename=self.input_file.name
|
||||
)
|
||||
)
|
||||
output_file = build_dir / DICTIONARY_NAMING_PATTERN.format(
|
||||
language=language,
|
||||
dict_name=self._dict_name
|
||||
)
|
||||
if not overwrite and output_file.exists():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
output_file.write_text(
|
||||
''.join(lines)
|
||||
)
|
||||
return [] # No future configuration needed
|
61
PyTeX/format/docstrip_formatter.py
Normal file
61
PyTeX/format/docstrip_formatter.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
import tempfile
|
||||
import os
|
||||
from .enums import TeXType
|
||||
from .formatting_config import FormattingConfig
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class DocStripFormatter(PyTeXFormatter):
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[str]:
|
||||
if self.config.tex_out_type == TeXType.TeXClass:
|
||||
return [self.raw_name + '.cls']
|
||||
elif self.config.tex_out_type == TeXType.TeXPackage:
|
||||
return [self.raw_name + '.sty']
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
tmp_dir: Path = Path(tempfile.mkdtemp())
|
||||
shutil.copy(
|
||||
self.input_file,
|
||||
tmp_dir
|
||||
)
|
||||
if self.input_file.with_suffix('.ins').exists():
|
||||
shutil.copy(
|
||||
self.input_file.with_suffix('.ins'),
|
||||
tmp_dir
|
||||
)
|
||||
result = subprocess.run(
|
||||
["tex", self.input_file.with_suffix('.ins').name],
|
||||
cwd=tmp_dir,
|
||||
stderr=subprocess.DEVNULL, # TODO
|
||||
stdout=subprocess.DEVNULL # TODO
|
||||
)
|
||||
if not result.returncode == 0:
|
||||
raise NotImplementedError('no correct returncode')
|
||||
else:
|
||||
result = subprocess.run(
|
||||
['pdflatex', self.input_file.name],
|
||||
cwd=tmp_dir,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
if not result.returncode == 0:
|
||||
raise NotImplementedError
|
||||
for file in self.output_files:
|
||||
outfile = tmp_dir / file
|
||||
if not outfile.exists():
|
||||
raise NotImplementedError(f'output file {outfile} does not exist')
|
||||
shutil.copy(outfile, build_dir)
|
||||
shutil.rmtree(tmp_dir)
|
||||
return [] # No future config
|
||||
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # TODO
|
111
PyTeX/format/dtx_formatter.py
Normal file
111
PyTeX/format/dtx_formatter.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
from .constants import INS_FILE, DRV_FILE
|
||||
from .enums import TeXFlavour, FormatterProperty, TeXType, FormatterMode
|
||||
from .generic_text import GenericText
|
||||
from .tex_formatter import TexFormatter
|
||||
from typing import List, Tuple
|
||||
from pathlib import Path
|
||||
from .formatting_config import FormattingConfig
|
||||
|
||||
|
||||
class DTXFormatter(TexFormatter):
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # TODO
|
||||
|
||||
@property
|
||||
def future_config(self) -> List[Tuple[str, FormattingConfig]]:
|
||||
config = FormattingConfig()
|
||||
config.tex_out_type = self.config.tex_out_type
|
||||
return [(self.name, config)]
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[str]:
|
||||
files = [self.name + '.dtx']
|
||||
if self.config.include_drv:
|
||||
files.append(self.name + '.drv')
|
||||
if self.config.include_ins:
|
||||
files.append(self.name + '.ins')
|
||||
return files
|
||||
|
||||
def _get_internal_file(self, comment: bool) -> str:
|
||||
g = GenericText(INS_FILE)
|
||||
switcher = {
|
||||
TeXType.TeXPackage: '.sty',
|
||||
TeXType.TeXClass: '.cls'
|
||||
}
|
||||
return g.format(
|
||||
infile=self.name + '.dtx',
|
||||
outfile=self.name + switcher[self.config.tex_out_type],
|
||||
preamble='', # TODO
|
||||
postamble='', # TODO
|
||||
guards=', '.join(self.config.docstrip_guards),
|
||||
padding=False,
|
||||
comment=comment
|
||||
)
|
||||
|
||||
def _get_drv_file(self, comment: bool) -> str:
|
||||
g = GenericText(DRV_FILE)
|
||||
return g.format(
|
||||
documentclass='l3doc', # TODO
|
||||
preamble='', # TODO
|
||||
infile=self.name + '.dtx',
|
||||
padding=False,
|
||||
comment=comment
|
||||
)
|
||||
|
||||
def format_pre_header(self) -> None:
|
||||
if self.current_file_name().endswith('.dtx'):
|
||||
self._shipout_line(r'% \iffalse meta-comment')
|
||||
|
||||
def format_post_header(self) -> None:
|
||||
if self.current_file_name().endswith('.dtx'):
|
||||
self._shipout_line('%<*internal>')
|
||||
self._shipout_line(
|
||||
self._get_internal_file(comment=True)
|
||||
)
|
||||
self._shipout_line('%</internal>')
|
||||
self._shipout_line('%')
|
||||
provides = self._get_provides_text(
|
||||
self.config.tex_out_type.value.capitalize()
|
||||
)
|
||||
parts = provides.split('\n')
|
||||
parts = [
|
||||
'%<{outtype}>'.format(
|
||||
outtype=self.config.tex_out_type.value
|
||||
) + part
|
||||
for part in parts
|
||||
]
|
||||
self._shipout_line(
|
||||
'\n'.join(parts)
|
||||
)
|
||||
self._shipout_line('%')
|
||||
self._shipout_line('%<*driver>')
|
||||
self._shipout_line(
|
||||
self._get_drv_file(comment=False)
|
||||
)
|
||||
self._shipout_line('%</driver>')
|
||||
self._shipout_line(r'% \fi')
|
||||
self._shipout_line('%')
|
||||
self.mode = FormatterMode.meta
|
||||
pass
|
||||
elif self.current_file_name().endswith('.ins'):
|
||||
self._shipout_line(
|
||||
self._get_internal_file(comment=False)
|
||||
)
|
||||
elif self.current_file_name().endswith('.drv'):
|
||||
self._shipout_line(
|
||||
self._get_drv_file(comment=False)
|
||||
)
|
||||
|
||||
def _post_process_line(self, line: str) -> str:
|
||||
line = line.rstrip(' %\n')
|
||||
if self.mode == FormatterMode.meta:
|
||||
line = line.lstrip('%')
|
||||
if line.startswith(' '):
|
||||
line = line[1:]
|
||||
if self.mode == FormatterMode.meta:
|
||||
line = '% ' + line
|
||||
if self.mode == FormatterMode.macrocode:
|
||||
if self.config.tex_flavour == TeXFlavour.LaTeX2e:
|
||||
line = line.rstrip(' %\n') + '%'
|
||||
return line
|
156
PyTeX/format/enums.py
Normal file
156
PyTeX/format/enums.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
from __future__ import annotations
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class FormatterMode(Enum):
|
||||
normal = 0
|
||||
normal_drop = 1
|
||||
macrocode = 2
|
||||
macrocode_drop = 3
|
||||
meta = 4
|
||||
meta_drop = 5
|
||||
|
||||
def to_drop(self):
|
||||
switcher = {
|
||||
FormatterMode.normal: FormatterMode.normal_drop,
|
||||
FormatterMode.macrocode: FormatterMode.macrocode_drop,
|
||||
FormatterMode.meta: FormatterMode.meta_drop
|
||||
}
|
||||
return switcher[self]
|
||||
|
||||
def to_undrop(self):
|
||||
switcher = {
|
||||
FormatterMode.normal_drop: FormatterMode.normal,
|
||||
FormatterMode.macrocode_drop: FormatterMode.macrocode,
|
||||
FormatterMode.meta_drop: FormatterMode.meta
|
||||
}
|
||||
return switcher[self]
|
||||
|
||||
def is_drop(self) -> bool:
|
||||
return self.value in [
|
||||
FormatterMode.normal_drop.value,
|
||||
FormatterMode.macrocode_drop.value,
|
||||
FormatterMode.meta_drop.value
|
||||
]
|
||||
|
||||
|
||||
class NamingScheme(Enum):
|
||||
prepend_author = 'prepend_author'
|
||||
clean = 'clean'
|
||||
|
||||
@staticmethod
|
||||
def parse(naming_scheme: str) -> Optional[NamingScheme]:
|
||||
if naming_scheme is None:
|
||||
return None
|
||||
switcher = {
|
||||
'prepend-author': NamingScheme.prepend_author,
|
||||
'prepend author': NamingScheme.prepend_author,
|
||||
'author': NamingScheme.prepend_author,
|
||||
'clean': NamingScheme.clean,
|
||||
'raw': NamingScheme.clean
|
||||
}
|
||||
if not naming_scheme in switcher.keys():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return switcher[naming_scheme]
|
||||
|
||||
|
||||
class TeXType(Enum):
|
||||
TeXPackage = 'package'
|
||||
TeXClass = 'class'
|
||||
TeXDocstrip = 'docstrip file'
|
||||
TeXDictionary = 'dictionary'
|
||||
TeXDocumentation = 'documentation'
|
||||
|
||||
@staticmethod
|
||||
def parse(tex_type: str) -> Optional[TeXType]:
|
||||
if tex_type is None:
|
||||
return None
|
||||
switcher = {
|
||||
'package': TeXType.TeXPackage,
|
||||
'sty': TeXType.TeXPackage,
|
||||
'class': TeXType.TeXClass,
|
||||
'cls': TeXType.TeXClass,
|
||||
'dictionary': TeXType.TeXDictionary,
|
||||
'dict': TeXType.TeXDictionary,
|
||||
'documentation': TeXType.TeXDocumentation,
|
||||
'doc': TeXType.TeXDocumentation, # TODO: dangerous?
|
||||
'dtx': TeXType.TeXDocstrip,
|
||||
'docstrip': TeXType.TeXDocstrip,
|
||||
'strip': TeXType.TeXDocstrip
|
||||
}
|
||||
if tex_type not in switcher.keys():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return switcher[tex_type]
|
||||
|
||||
|
||||
class TeXFlavour(Enum):
|
||||
TeX = 'TeX'
|
||||
LaTeX2e = 'LaTeX2e'
|
||||
LaTeX3 = 'LaTeX3'
|
||||
|
||||
@staticmethod
|
||||
def parse(flavour: str) -> Optional[TeXFlavour]:
|
||||
if flavour is None:
|
||||
return None
|
||||
switcher = {
|
||||
'1': TeXFlavour.TeX,
|
||||
'2': TeXFlavour.LaTeX2e,
|
||||
'2e': TeXFlavour.LaTeX2e,
|
||||
'3': TeXFlavour.LaTeX3,
|
||||
'TeX': TeXFlavour.TeX,
|
||||
'LaTeX2e': TeXFlavour.LaTeX2e,
|
||||
'LaTeX2': TeXFlavour.LaTeX2e,
|
||||
'LaTeX3': TeXFlavour.LaTeX3,
|
||||
}
|
||||
if flavour not in switcher.keys():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return switcher[flavour]
|
||||
|
||||
|
||||
class MacroReplacementAtomIF:
|
||||
pass
|
||||
|
||||
|
||||
class FormatterProperty(MacroReplacementAtomIF, Enum):
|
||||
author = 'author'
|
||||
shortauthor = 'shortauthor'
|
||||
date = 'date'
|
||||
year = 'year'
|
||||
raw_name = 'raw_name' # The 'raw' name of the package, without author prefix
|
||||
name = 'name' # class or package name
|
||||
file_prefix = 'file_prefix'
|
||||
version = 'version'
|
||||
file_name = 'file_name'
|
||||
source_file_name = 'source_file_name'
|
||||
repo_version = 'repo_version'
|
||||
repo_branch = 'repo_branch'
|
||||
repo_commit = 'repo_commit'
|
||||
repo_dirty = 'repo_dirty'
|
||||
pytex_version = 'pytex_version'
|
||||
pytex_branch = 'pytex_branch'
|
||||
pytex_commit = 'pytex_commit'
|
||||
pytex_dirty = 'pytex_dirty'
|
||||
tex_type = 'tex_type'
|
||||
Tex_type = 'Tex_type'
|
||||
tex_out_type = 'tex_outtype'
|
||||
tex_flavour = 'latex_flavour'
|
||||
description = 'description'
|
||||
|
||||
|
||||
class Argument(MacroReplacementAtomIF, Enum):
|
||||
one = 1
|
||||
two = 2
|
||||
three = 3
|
||||
four = 4
|
||||
five = 5
|
||||
six = 6
|
||||
|
||||
|
||||
class Target(Enum):
|
||||
tex_source = 1
|
||||
tex = 2
|
||||
documentation = 3
|
50
PyTeX/format/errors.py
Normal file
50
PyTeX/format/errors.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from typing import Optional
|
||||
|
||||
|
||||
class PyTeXError(Exception):
|
||||
def __init__(self, msg, explanation: Optional[str] = None):
|
||||
self._msg = msg
|
||||
self._traceback_explanation = []
|
||||
if explanation is not None:
|
||||
self._traceback_explanation.append(explanation)
|
||||
super().__init__(self._dispstr())
|
||||
|
||||
def _dispstr(self):
|
||||
depth = 0
|
||||
ret = [self._msg]
|
||||
for explanation in self._traceback_explanation:
|
||||
ret.append(' ' * 2 * depth + explanation)
|
||||
depth += 1
|
||||
return '\n'.join(ret)
|
||||
|
||||
def add_explanation(self, explanation: str):
|
||||
self._traceback_explanation.append(explanation)
|
||||
|
||||
def __str__(self):
|
||||
return self._dispstr()
|
||||
|
||||
|
||||
class PyTeXFormattingError(PyTeXError):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXBuildError(PyTeXError):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXMacroError(PyTeXError):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXInvalidMacroUsageError(PyTeXMacroError):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXInvalidBeginMacroCodeUsageError(PyTeXInvalidMacroUsageError):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXInvalidEndMacroCodeUsageError(PyTeXInvalidMacroUsageError):
|
||||
pass
|
||||
|
||||
|
66
PyTeX/format/formatterif.py
Normal file
66
PyTeX/format/formatterif.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .config import Config
|
||||
from .formatting_config import FormattingConfig
|
||||
|
||||
|
||||
class FormatterIF(ABC):
|
||||
"""
|
||||
A formatter is bound to a specific input file with some
|
||||
building configuration.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_file: Optional[Path] = None,
|
||||
config: Optional[Config] = None,
|
||||
):
|
||||
self._input_file: Optional[Path] = input_file
|
||||
self._config: Optional[Config] = config
|
||||
|
||||
@abstractmethod
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
"""
|
||||
:param build_dir: Directory where output files are written to
|
||||
:param overwrite: overwrite existing files
|
||||
:return: When configuration files are needed for a future
|
||||
build of the output files, a list of the file names and their
|
||||
needed configurations. Else None.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def output_files(self) -> List[str]:
|
||||
"""
|
||||
|
||||
:return: List of files that will be built when the formatter is invoked
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def dependencies(self) -> List[str]:
|
||||
"""
|
||||
:return: List of dependencies (as str filenames)
|
||||
"""
|
||||
|
||||
@property
|
||||
def input_file(self) -> Path:
|
||||
if self._input_file is None:
|
||||
raise NotImplementedError
|
||||
return self._input_file
|
||||
|
||||
@input_file.setter
|
||||
def input_file(self, input_file: Path) -> None:
|
||||
self._input_file = input_file
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
if self._config is None:
|
||||
raise NotImplementedError
|
||||
return self._config
|
||||
|
||||
@config.setter
|
||||
def config(self, config: Config):
|
||||
self._config = config
|
400
PyTeX/format/formatting_config.py
Normal file
400
PyTeX/format/formatting_config.py
Normal file
|
@ -0,0 +1,400 @@
|
|||
from typing import List, Optional, Dict
|
||||
|
||||
from .constants import *
|
||||
from .enums import NamingScheme
|
||||
from .enums import TeXType, TeXFlavour
|
||||
from .generic_text import GenericText
|
||||
from .config import Config
|
||||
|
||||
|
||||
class FormattingConfig(Config):
|
||||
def __init__(self):
|
||||
self._naming_scheme: Optional[NamingScheme] = None
|
||||
self._license: Optional[GenericText] = None
|
||||
self._description: Optional[str] = None
|
||||
|
||||
self._include_extra_header: Optional[bool] = None
|
||||
self._include_pytex_version: Optional[bool] = None
|
||||
self._include_pytex_info_text: Optional[bool] = None
|
||||
self._include_repo_version: Optional[bool] = None
|
||||
self._include_repo_info_text: Optional[bool] = None
|
||||
self._include_time: Optional[bool] = None
|
||||
self._include_license: Optional[bool] = None
|
||||
|
||||
self._extra_header: Optional[GenericText] = None
|
||||
self._author: Optional[str] = None
|
||||
self._version: Optional[str] = None
|
||||
self._pytex_info_text: Optional[GenericText] = None
|
||||
self._repo_info_text: Optional[GenericText] = None
|
||||
|
||||
self._include_drv: Optional[bool] = None
|
||||
self._include_ins: Optional[bool] = None
|
||||
self._docstrip_guards: Optional[List[str]] = None
|
||||
|
||||
self._doc_dependencies: Optional[List[str]] = None
|
||||
self._tex_dependencies: Optional[List[str]] = None
|
||||
|
||||
self._tex_type: Optional[TeXType] = None
|
||||
self._tex_out_type: Optional[TeXType] = None
|
||||
self._tex_flavour: Optional[TeXFlavour] = None
|
||||
|
||||
self._escape_character: Optional[str] = None
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content = self._fill_keys(content)
|
||||
|
||||
info = filled_content[YAML_INFO]
|
||||
self._author = info[YAML_AUTHOR]
|
||||
self._naming_scheme = NamingScheme.parse(info[YAML_NAMING_SCHEME])
|
||||
self._tex_flavour = TeXFlavour.parse(info[YAML_TEX_FLAVOUR])
|
||||
self._tex_type = TeXType.parse(info[YAML_TEX_TYPE])
|
||||
self._tex_out_type = TeXType.parse(info[YAML_TEX_OUT_TYPE])
|
||||
self._description = info[YAML_DESCRIPTION]
|
||||
self._version = info[YAML_VERSION]
|
||||
|
||||
header = filled_content[YAML_HEADER]
|
||||
extra = header[YAML_EXTRA]
|
||||
self._include_extra_header = extra[YAML_INCLUDE_EXTRA_HEADER]
|
||||
if extra[YAML_PATH] or extra[YAML_TEXT]:
|
||||
self._extra_header = GenericText(
|
||||
extra[YAML_PATH] if extra[YAML_PATH] else extra[YAML_TEXT]
|
||||
)
|
||||
|
||||
repo = header[YAML_REPO]
|
||||
self._include_repo_info_text = repo[YAML_INCLUDE_INFO_TEXT]
|
||||
self._include_repo_version = repo[YAML_INCLUDE_VERSION]
|
||||
if repo[YAML_PATH] or repo[YAML_TEXT]:
|
||||
self._repo_info_text = GenericText(
|
||||
repo[YAML_PATH] if repo[YAML_PATH] else repo[YAML_TEXT]
|
||||
)
|
||||
|
||||
pytex = header[YAML_PYTEX]
|
||||
self._include_pytex_info_text = pytex[YAML_INCLUDE_INFO_TEXT]
|
||||
self._include_pytex_version = pytex[YAML_INCLUDE_VERSION]
|
||||
if pytex[YAML_PATH] or pytex[YAML_TEXT]:
|
||||
self._pytex_info_text = GenericText(
|
||||
pytex[YAML_PATH] if pytex[YAML_PATH] else pytex[YAML_TEXT]
|
||||
)
|
||||
|
||||
self._include_time = header[YAML_INCLUDE_TIME]
|
||||
|
||||
license_ = header[YAML_LICENSE]
|
||||
self._include_license = license_[YAML_INCLUDE_LICENSE]
|
||||
if license_[YAML_PATH] or license_[YAML_TEXT]:
|
||||
self._license = GenericText(
|
||||
license_[YAML_PATH] if license_[YAML_PATH] else license_[YAML_TEXT]
|
||||
)
|
||||
|
||||
docstrip = filled_content[YAML_DOCSTRIP]
|
||||
self._include_drv = docstrip[YAML_INCLUDE_DRV]
|
||||
self._include_ins = docstrip[YAML_INCLUDE_INS]
|
||||
self._docstrip_guards = docstrip[YAML_DOCSTRIP_GUARDS]
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
YAML_INFO: {
|
||||
YAML_AUTHOR: self._author,
|
||||
YAML_NAMING_SCHEME: self._naming_scheme.value if self._naming_scheme else None,
|
||||
YAML_TEX_FLAVOUR: self._tex_flavour.value if self._tex_flavour else None,
|
||||
YAML_TEX_TYPE: self._tex_type.value if self._tex_type else None,
|
||||
YAML_TEX_OUT_TYPE: self._tex_out_type.value if self._tex_out_type else None,
|
||||
YAML_VERSION: self._version,
|
||||
YAML_DESCRIPTION: self._description
|
||||
},
|
||||
YAML_HEADER: {
|
||||
YAML_EXTRA: {
|
||||
YAML_INCLUDE_EXTRA_HEADER: self._include_extra_header,
|
||||
YAML_PATH: self._extra_header.pathname if self._extra_header else None,
|
||||
YAML_TEXT: self._extra_header.real_text if self._extra_header else None
|
||||
},
|
||||
YAML_REPO: {
|
||||
YAML_INCLUDE_INFO_TEXT: self._include_repo_info_text,
|
||||
YAML_INCLUDE_VERSION: self._include_repo_version,
|
||||
YAML_PATH: self._repo_info_text.pathname if self._repo_info_text else None,
|
||||
YAML_TEXT: self._repo_info_text.real_text if self._repo_info_text else None
|
||||
},
|
||||
YAML_PYTEX: {
|
||||
YAML_INCLUDE_INFO_TEXT: self._include_pytex_info_text,
|
||||
YAML_INCLUDE_VERSION: self._include_pytex_version,
|
||||
YAML_PATH: self._pytex_info_text.path if self._pytex_info_text else None,
|
||||
YAML_TEXT: self._pytex_info_text.real_text if self._pytex_info_text else None
|
||||
},
|
||||
YAML_INCLUDE_TIME: self._include_time,
|
||||
YAML_LICENSE: {
|
||||
YAML_INCLUDE_LICENSE: self._include_license,
|
||||
YAML_PATH: self._license.path if self._license else None,
|
||||
YAML_TEXT: self._license.real_text if self._license else None
|
||||
},
|
||||
},
|
||||
YAML_DOCSTRIP: {
|
||||
YAML_INCLUDE_DRV: self._include_drv,
|
||||
YAML_INCLUDE_INS: self._include_ins,
|
||||
YAML_DOCSTRIP_GUARDS: self._docstrip_guards
|
||||
},
|
||||
YAML_DEPENDENCIES: {
|
||||
YAML_DOC_DEPENDENCIES: self._doc_dependencies,
|
||||
YAML_TEX_DEPENDENCIES: self._tex_dependencies
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def has_description(self) -> bool:
|
||||
return self._description is not None
|
||||
|
||||
@property
|
||||
def naming_scheme(self) -> NamingScheme:
|
||||
if self._naming_scheme is None:
|
||||
return NamingScheme.prepend_author
|
||||
else:
|
||||
return self._naming_scheme
|
||||
|
||||
@naming_scheme.setter
|
||||
def naming_scheme(self, naming_scheme: NamingScheme):
|
||||
self._naming_scheme = naming_scheme
|
||||
|
||||
@property
|
||||
def license(self) -> GenericText:
|
||||
if self._license is None:
|
||||
return GenericText()
|
||||
else:
|
||||
return self._license
|
||||
|
||||
@license.setter
|
||||
def license(self, license_: GenericText):
|
||||
self._license = license_
|
||||
|
||||
@property
|
||||
def include_extra_header(self) -> bool:
|
||||
if self._include_extra_header is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_extra_header
|
||||
|
||||
@include_extra_header.setter
|
||||
def include_extra_header(self, include: bool):
|
||||
self._include_extra_header = include
|
||||
|
||||
@property
|
||||
def include_pytex_version(self) -> bool:
|
||||
if self._include_pytex_version is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_pytex_version
|
||||
|
||||
@include_pytex_version.setter
|
||||
def include_pytex_version(self, include: bool):
|
||||
self._include_pytex_version = include
|
||||
|
||||
@property
|
||||
def include_pytex_info_text(self) -> bool:
|
||||
if self._include_pytex_info_text is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_pytex_info_text
|
||||
|
||||
@include_pytex_info_text.setter
|
||||
def include_pytex_info_text(self, include: bool):
|
||||
self._include_pytex_info_text = include
|
||||
|
||||
@property
|
||||
def include_repo_version(self) -> bool:
|
||||
if self._include_repo_version is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_repo_version
|
||||
|
||||
@include_repo_version.setter
|
||||
def include_repo_version(self, include: bool):
|
||||
self._include_repo_version = include
|
||||
|
||||
@property
|
||||
def include_repo_info_text(self) -> bool:
|
||||
if self._include_repo_info_text is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_repo_info_text
|
||||
|
||||
@include_repo_info_text.setter
|
||||
def include_repo_info_text(self, include: bool):
|
||||
self._include_repo_info_text = include
|
||||
|
||||
@property
|
||||
def extra_header(self) -> GenericText:
|
||||
if self._extra_header is None:
|
||||
return GenericText()
|
||||
else:
|
||||
return self._extra_header
|
||||
|
||||
@extra_header.setter
|
||||
def extra_header(self, extra_header: GenericText):
|
||||
self._extra_header = extra_header
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
if self._author is None:
|
||||
return "MISSING AUTHOR"
|
||||
else:
|
||||
return self._author
|
||||
|
||||
@author.setter
|
||||
def author(self, author: str):
|
||||
self._author = author
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
if self._version is None:
|
||||
return "0.0.0"
|
||||
else:
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, version: str):
|
||||
self._version = version
|
||||
|
||||
@property
|
||||
def pytex_info_text(self) -> GenericText:
|
||||
if self._pytex_info_text is None:
|
||||
return GenericText()
|
||||
else:
|
||||
return self._pytex_info_text
|
||||
|
||||
@pytex_info_text.setter
|
||||
def pytex_info_text(self, info_text: GenericText):
|
||||
self._pytex_info_text = info_text
|
||||
|
||||
@property
|
||||
def repo_info_text(self) -> GenericText:
|
||||
if self._repo_info_text is None:
|
||||
return GenericText()
|
||||
else:
|
||||
return self._repo_info_text
|
||||
|
||||
@repo_info_text.setter
|
||||
def repo_info_text(self, info_text: GenericText):
|
||||
self._repo_info_text = info_text
|
||||
|
||||
@property
|
||||
def include_drv(self) -> bool:
|
||||
if self._include_drv is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_drv
|
||||
|
||||
@include_drv.setter
|
||||
def include_drv(self, include: bool):
|
||||
self._include_drv = include
|
||||
|
||||
@property
|
||||
def include_ins(self) -> bool:
|
||||
if self._include_ins is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_ins
|
||||
|
||||
@include_ins.setter
|
||||
def include_ins(self, include):
|
||||
self._include_ins = include
|
||||
|
||||
@property
|
||||
def docstrip_guards(self) -> List[str]:
|
||||
if self._docstrip_guards is None:
|
||||
if self.tex_type in [TeXType.TeXDocstrip]:
|
||||
return [self.tex_type.value]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return self._docstrip_guards
|
||||
|
||||
@docstrip_guards.setter
|
||||
def docstrip_guards(self, guards: List[str]):
|
||||
self._docstrip_guards = guards
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
if self._description is None:
|
||||
return ''
|
||||
else:
|
||||
return self._description
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str):
|
||||
self._description = description
|
||||
|
||||
@property
|
||||
def include_time(self) -> bool:
|
||||
if self._include_time is None:
|
||||
return False
|
||||
else:
|
||||
return self._include_time
|
||||
|
||||
@include_time.setter
|
||||
def include_time(self, include: bool):
|
||||
self._include_time = include
|
||||
|
||||
@property
|
||||
def doc_dependencies(self) -> List[str]:
|
||||
if self._doc_dependencies is None:
|
||||
return []
|
||||
else:
|
||||
return self._doc_dependencies
|
||||
|
||||
@doc_dependencies.setter
|
||||
def doc_dependencies(self, dependencies: List[str]):
|
||||
self._doc_dependencies = dependencies
|
||||
|
||||
@property
|
||||
def tex_dependencies(self) -> List[str]:
|
||||
if self._tex_dependencies is None:
|
||||
return []
|
||||
else:
|
||||
return self._tex_dependencies
|
||||
|
||||
@tex_dependencies.setter
|
||||
def tex_dependencies(self, dependencies: List[str]):
|
||||
self._tex_dependencies = dependencies
|
||||
|
||||
@property
|
||||
def escape_character(self) -> str:
|
||||
if self._escape_character is None:
|
||||
return '!'
|
||||
else:
|
||||
return self._escape_character
|
||||
|
||||
@property
|
||||
def tex_type(self) -> TeXType:
|
||||
if self._tex_type is None:
|
||||
return TeXType.TeXClass
|
||||
else:
|
||||
return self._tex_type
|
||||
|
||||
@tex_type.setter
|
||||
def tex_type(self, tex_type: TeXType):
|
||||
self._tex_type = tex_type
|
||||
|
||||
@property
|
||||
def tex_flavour(self) -> TeXFlavour:
|
||||
if self._tex_flavour is None:
|
||||
return TeXFlavour.LaTeX2e
|
||||
else:
|
||||
return self._tex_flavour
|
||||
|
||||
@tex_flavour.setter
|
||||
def tex_flavour(self, tex_flavour: TeXFlavour) -> None:
|
||||
self._tex_flavour = tex_flavour
|
||||
|
||||
@property
|
||||
def tex_out_type(self) -> TeXType:
|
||||
if self._tex_out_type is None:
|
||||
return TeXType.TeXPackage
|
||||
else:
|
||||
return self._tex_out_type
|
||||
|
||||
@tex_out_type.setter
|
||||
def tex_out_type(self, value):
|
||||
self._tex_out_type = value
|
||||
|
||||
|
||||
class DocFormattingConfig:
|
||||
def __init__(self):
|
||||
self._documents: Optional[List[str]] = None
|
||||
self._dependencies: Optional[List[str]] = None
|
125
PyTeX/format/generic_text.py
Normal file
125
PyTeX/format/generic_text.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Union, List, Optional
|
||||
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
class GenericText:
|
||||
def __init__(self, content: Optional[Union[List[str], Path, str]] = None):
|
||||
# TODO: what if paths are not absolute? Have a root available?
|
||||
if isinstance(content, list):
|
||||
self._content: Optional[List[str]] = content
|
||||
self._path = None
|
||||
self._initialized = True
|
||||
elif isinstance(content, Path) or isinstance(content, str):
|
||||
self._content = None
|
||||
self._path = Path(content)
|
||||
self._initialized = True
|
||||
else:
|
||||
self._content = None
|
||||
self._path = None
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def text(self) -> List[str]:
|
||||
if self._initialized:
|
||||
if self._content is None:
|
||||
if self._path is None:
|
||||
raise NotImplementedError # Programming error
|
||||
try:
|
||||
with open(self._path, 'r') as file:
|
||||
self._content = file.readlines()
|
||||
except FileNotFoundError:
|
||||
raise NotImplementedError
|
||||
except:
|
||||
raise NotImplementedError
|
||||
return self._content
|
||||
else:
|
||||
return []
|
||||
|
||||
@text.setter
|
||||
def text(self, content: Union[List[str], Path, None]) -> None:
|
||||
if isinstance(content, List):
|
||||
self._content = content
|
||||
self._path = None
|
||||
self._initialized = True
|
||||
elif isinstance(content, Path):
|
||||
self._content = None
|
||||
self._path = content
|
||||
self._initialized = True
|
||||
else:
|
||||
self._content = None
|
||||
self._path = None
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def path(self) -> Optional[Path]:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def pathname(self) -> Optional[str]:
|
||||
return str(self._path) if self._path else None
|
||||
|
||||
def format(self, **kwargs) -> str:
|
||||
padding = True
|
||||
comment = True
|
||||
if 'padding' in kwargs.keys():
|
||||
padding = kwargs['padding']
|
||||
kwargs.pop('padding', None)
|
||||
if 'comment' in kwargs.keys():
|
||||
comment = kwargs['comment']
|
||||
kwargs.pop('comment', None)
|
||||
lines = []
|
||||
for line in self.text:
|
||||
try:
|
||||
line = line.rstrip().format(**kwargs)
|
||||
if comment:
|
||||
line = '% ' + line
|
||||
if padding:
|
||||
line = line.ljust(79) + '%'
|
||||
if len(line) > 80:
|
||||
logger.warning(
|
||||
'Line too long') # TODO
|
||||
lines.append(line)
|
||||
except ValueError:
|
||||
raise NotImplementedError
|
||||
return '\n'.join(lines)
|
||||
|
||||
def has_value(self) -> bool:
|
||||
return self._initialized
|
||||
|
||||
@property
|
||||
def real_text(self) -> Optional[List[str]]:
|
||||
if self.has_value():
|
||||
return self._content
|
||||
else:
|
||||
return None
|
||||
|
||||
def __add__(self, other: Union[None, GenericText, List[str]]) -> GenericText:
|
||||
if not self.has_value():
|
||||
return other
|
||||
if isinstance(other, GenericText):
|
||||
if not other.has_value():
|
||||
return self
|
||||
return GenericText(self.text + other.text)
|
||||
else:
|
||||
return GenericText(self.text + other)
|
||||
|
||||
def __iadd__(self, other: Union[None, GenericText, List[str]]) -> GenericText:
|
||||
if not self.has_value():
|
||||
if isinstance(other, GenericText):
|
||||
self.text = other.text
|
||||
else:
|
||||
self.text = other
|
||||
elif isinstance(other, GenericText):
|
||||
if other.has_value():
|
||||
self.text += other.text
|
||||
else:
|
||||
self.text += other
|
||||
return self
|
||||
|
||||
def __radd__(self, other):
|
||||
if other is None:
|
||||
return self
|
||||
return other + self
|
57
PyTeX/format/git_version_info.py
Normal file
57
PyTeX/format/git_version_info.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
from typing import Optional, Dict
|
||||
|
||||
from PyTeX.format.config import Config
|
||||
from PyTeX.format.constants import YAML_REPO, YAML_PYTEX
|
||||
from PyTeX.format.repo_status_info import RepoStatusInfo
|
||||
from .repo_status_info import RepoStatusInfo
|
||||
|
||||
|
||||
class GitVersionInfo(Config):
|
||||
def __init__(self):
|
||||
self._repo_version: Optional[RepoStatusInfo] = None
|
||||
self._pytex_version: Optional[RepoStatusInfo] = None
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content = self._fill_keys(content)
|
||||
self._repo_version = RepoStatusInfo.from_json(
|
||||
filled_content[YAML_REPO]
|
||||
)
|
||||
self._pytex_version = RepoStatusInfo.from_json(
|
||||
filled_content[YAML_PYTEX]
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
YAML_PYTEX: self.pytex_version.to_json(),
|
||||
YAML_REPO: self.repo_version.to_json()
|
||||
}
|
||||
|
||||
@property
|
||||
def pytex_version(self) -> RepoStatusInfo:
|
||||
if self._pytex_version is None:
|
||||
return RepoStatusInfo()
|
||||
else:
|
||||
return self._pytex_version
|
||||
|
||||
@pytex_version.setter
|
||||
def pytex_version(self, pytex_version: RepoStatusInfo):
|
||||
self._pytex_version = pytex_version
|
||||
|
||||
@property
|
||||
def repo_version(self) -> RepoStatusInfo:
|
||||
if self._repo_version is None:
|
||||
return RepoStatusInfo()
|
||||
else:
|
||||
return self._repo_version
|
||||
|
||||
@repo_version.setter
|
||||
def repo_version(self, repo_version: RepoStatusInfo):
|
||||
self._repo_version = repo_version
|
||||
|
||||
@property
|
||||
def has_pytex_version(self) -> bool:
|
||||
return self._pytex_version is not None
|
||||
|
||||
@property
|
||||
def has_repo_version(self) -> bool:
|
||||
return self._repo_version is not None
|
17
PyTeX/format/ins_formatter.py
Normal file
17
PyTeX/format/ins_formatter.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from .formatting_config import FormattingConfig
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class InsFormatter(PyTeXFormatter):
|
||||
def output_files(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
return []
|
||||
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # TODO
|
294
PyTeX/format/macros.py
Normal file
294
PyTeX/format/macros.py
Normal file
|
@ -0,0 +1,294 @@
|
|||
import re
|
||||
from typing import List, Union, Tuple, Dict
|
||||
|
||||
from .constants import *
|
||||
from .enums import FormatterProperty, Argument, FormatterMode
|
||||
from abc import ABC, abstractmethod
|
||||
from .errors import *
|
||||
|
||||
|
||||
class MacroReplacement:
|
||||
def __init__(
|
||||
self,
|
||||
replacement: str,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if 'format_type' in kwargs.keys():
|
||||
self.format_type = kwargs['format_type']
|
||||
else:
|
||||
self.format_type = '%'
|
||||
self.replacement: str = replacement
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def make_format_args(self, formatter, *call_args) -> Tuple[Tuple, Dict]:
|
||||
new_args = []
|
||||
for arg in self.args:
|
||||
if type(arg) == FormatterProperty:
|
||||
try:
|
||||
new_args.append(formatter.attribute_dict[arg.value])
|
||||
except:
|
||||
raise NotImplementedError
|
||||
elif type(arg) == Argument:
|
||||
try:
|
||||
new_args.append(call_args[arg.value - 1])
|
||||
except:
|
||||
raise NotImplementedError
|
||||
elif type(arg) == str:
|
||||
new_args.append(arg)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
new_kwargs = {}
|
||||
for kw in self.kwargs.keys():
|
||||
if type(self.kwargs[kw]) == FormatterProperty:
|
||||
new_kwargs[kw] = formatter.attribute_dict[self.kwargs[kw].value]
|
||||
elif type(self.kwargs[kw]) == Argument:
|
||||
new_kwargs[kw] = call_args[self.kwargs[kw].value - 1]
|
||||
elif type(self.kwargs[kw]) == str:
|
||||
new_kwargs[kw] = self.kwargs[kw]
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return tuple(new_args), new_kwargs
|
||||
|
||||
def format(self, formatter, *call_args) -> str:
|
||||
args, kwargs = self.make_format_args(formatter, *call_args)
|
||||
if self.format_type == '%':
|
||||
if self.kwargs:
|
||||
raise NotImplementedError # Currently, not supported
|
||||
return self.replacement % args
|
||||
elif self.format_type == '{':
|
||||
return self.replacement.format(
|
||||
*args, **kwargs, **formatter.attribute_dict
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Macro(ABC):
|
||||
@abstractmethod
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def matches(self, line: str) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def apply(self, line: str, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
"""
|
||||
|
||||
:param line: Line where macro matches
|
||||
:param formatter:
|
||||
:return: First: replacement. Second: indicates direct shipout if True
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SimpleMacro(Macro):
|
||||
def __init__(
|
||||
self,
|
||||
macroname: str,
|
||||
macro_replacement: MacroReplacement
|
||||
):
|
||||
self.macroname = macroname
|
||||
self.macro_replacement = macro_replacement
|
||||
|
||||
def matches(self, line: str) -> bool:
|
||||
return line.find(FORMATTER_PREFIX + self.macroname) != -1
|
||||
|
||||
def apply(self, line: str, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
return line.replace(
|
||||
FORMATTER_PREFIX + self.macroname,
|
||||
self.macro_replacement.format(
|
||||
formatter
|
||||
)), False
|
||||
|
||||
|
||||
class SingleLineMacro(Macro, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
strip: str = ' %\n'
|
||||
):
|
||||
self.strip = strip
|
||||
|
||||
@abstractmethod
|
||||
def _apply(self, line, formatter) -> Union[Union[str, List[str]], Tuple[Union[str, List[str]], bool]]:
|
||||
pass
|
||||
|
||||
def apply(self, line: str, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
replacement = self._apply(line, formatter)
|
||||
if isinstance(replacement, tuple):
|
||||
return replacement
|
||||
else:
|
||||
return replacement, True
|
||||
|
||||
@abstractmethod
|
||||
def _matches(self, line: str) -> Optional[str]:
|
||||
pass
|
||||
|
||||
def matches(self, line: str) -> bool:
|
||||
match = self._matches(line.strip(self.strip))
|
||||
if match is None:
|
||||
return False
|
||||
else:
|
||||
if not line.strip(self.strip) == match:
|
||||
raise NotImplementedError
|
||||
return True
|
||||
|
||||
|
||||
class RegexSingleLineMacro(SingleLineMacro, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
regex: str,
|
||||
strip: str = ' %\n'
|
||||
):
|
||||
self.regex = regex
|
||||
super(RegexSingleLineMacro, self).__init__(strip)
|
||||
|
||||
def _matches(self, line: str) -> Optional[str]:
|
||||
match = re.search(self.regex, line)
|
||||
if match is not None:
|
||||
return match.group()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class SimpleSingleLineMacro(SingleLineMacro, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
chars: str,
|
||||
strip: str = ' %\n'
|
||||
):
|
||||
self.chars = chars
|
||||
super(SimpleSingleLineMacro, self).__init__(strip)
|
||||
|
||||
def _matches(self, line: str) -> Optional[str]:
|
||||
return self.chars if self.chars in line else None
|
||||
|
||||
|
||||
class GuardMacro(RegexSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(GuardMacro, self).__init__(r'<(\*|/|@@=)[a-zA-Z_]*>')
|
||||
|
||||
def _apply(self, line, formatter) -> Union[str, List[str]]:
|
||||
match = re.search(self.regex, line)
|
||||
return '%' + match.group()
|
||||
|
||||
|
||||
class ConfigBeginMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(ConfigBeginMacro, self).__init__(FORMATTER_PREFIX + INFILE_CONFIG_BEGIN_CONFIG)
|
||||
|
||||
def _apply(self, line: str, formatter) -> Union[str, List[str]]:
|
||||
if formatter.mode.is_drop():
|
||||
raise NotImplementedError # invalid config begin
|
||||
formatter.mode = formatter.mode.to_drop()
|
||||
return []
|
||||
|
||||
|
||||
class ConfigEndMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(ConfigEndMacro, self).__init__(FORMATTER_PREFIX + INFILE_CONFIG_END_CONFIG)
|
||||
|
||||
def _apply(self, line: str, formatter) -> Union[str, List[str]]:
|
||||
if not formatter.mode.is_drop():
|
||||
raise NotImplementedError # invalid
|
||||
formatter.mode = formatter.mode.to_undrop()
|
||||
return []
|
||||
|
||||
|
||||
class ImplementationBeginMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(ImplementationBeginMacro, self).__init__(FORMATTER_PREFIX + IMPLEMENTATION_BEGIN_MACRO)
|
||||
|
||||
def _apply(self, line, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
return [
|
||||
r'% \begin{implementation}',
|
||||
r'',
|
||||
r'\section{\pkg{!name} implementation}',
|
||||
r'\begin{macrocode}',
|
||||
r'<*!outtype>',
|
||||
r'\end{macrocode}',
|
||||
r'',
|
||||
r'\begin{macrocode}',
|
||||
r'<@@=!!>',
|
||||
r'\end{macrocode}',
|
||||
], False
|
||||
|
||||
|
||||
class ImplementationEndMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(ImplementationEndMacro, self).__init__(FORMATTER_PREFIX + IMPLEMENTATION_END_MACRO)
|
||||
|
||||
def _apply(self, line, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
return [
|
||||
r'\begin{macrocode}',
|
||||
r'</!outtype>',
|
||||
r'\end{macrocode}',
|
||||
r'',
|
||||
r'% \end{implementation}'
|
||||
], False
|
||||
|
||||
|
||||
class MacroCodeBeginMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(MacroCodeBeginMacro, self).__init__(r'\begin{macrocode}')
|
||||
|
||||
def _apply(self, line: str, formatter) -> Union[str, List[str]]:
|
||||
if not formatter.mode == FormatterMode.meta:
|
||||
raise PyTeXInvalidBeginMacroCodeUsageError(
|
||||
r"\begin{macrocode} used outside meta context"
|
||||
)
|
||||
formatter.mode = FormatterMode.macrocode
|
||||
return r'% \begin{macrocode}'
|
||||
|
||||
|
||||
class MacroCodeEndMacro(SimpleSingleLineMacro):
|
||||
def __init__(self):
|
||||
super(MacroCodeEndMacro, self).__init__(r'\end{macrocode}')
|
||||
|
||||
def _apply(self, line: str, formatter) -> Union[str, List[str]]:
|
||||
if not formatter.mode == FormatterMode.macrocode:
|
||||
raise PyTeXInvalidEndMacroCodeUsageError(
|
||||
r"\end{macrocode} used outside macrocode context"
|
||||
)
|
||||
formatter.mode = FormatterMode.meta
|
||||
return r'% \end{macrocode}'
|
||||
|
||||
|
||||
class ArgumentMacro(Macro):
|
||||
def __init__(
|
||||
self,
|
||||
macroname: str,
|
||||
num_args: int,
|
||||
macro_replacement: MacroReplacement
|
||||
):
|
||||
self.macroname = macroname
|
||||
self.num_args = num_args
|
||||
self.macro_replacement: MacroReplacement = macro_replacement
|
||||
self._search_regex = re.compile(r'{keyword}\({arguments}(?<!@)\)'.format(
|
||||
keyword=FORMATTER_PREFIX + self.macroname,
|
||||
arguments=','.join(['(.*?)'] * self.num_args)
|
||||
))
|
||||
|
||||
def matches(self, line: str) -> bool:
|
||||
if line.find('!!') != -1:
|
||||
pass
|
||||
match = re.search(self._search_regex, line)
|
||||
if match is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def apply(self, line: str, formatter) -> Tuple[Union[str, List[str]], bool]:
|
||||
match = re.search(self._search_regex, line)
|
||||
if match is None:
|
||||
raise NotImplementedError
|
||||
replacement = self.macro_replacement.format(
|
||||
formatter, match.groups()
|
||||
)
|
||||
return line.replace(
|
||||
match.group(),
|
||||
replacement
|
||||
), False
|
25
PyTeX/format/nothing_formatter.py
Normal file
25
PyTeX/format/nothing_formatter.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from .formatting_config import FormattingConfig
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class NothingFormatter(PyTeXFormatter):
|
||||
"""
|
||||
Class that will represent a source file that
|
||||
should not be formatted.
|
||||
|
||||
This is modeled by not having any output files,
|
||||
so the builder will never consider building this file
|
||||
"""
|
||||
|
||||
def output_files(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def dependencies(self) -> List[str]:
|
||||
return []
|
219
PyTeX/format/pytex_formatter.py
Normal file
219
PyTeX/format/pytex_formatter.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, TextIO
|
||||
|
||||
from .constants import *
|
||||
from .formatterif import FormatterIF
|
||||
from .formatting_config import FormattingConfig
|
||||
from .git_version_info import GitVersionInfo
|
||||
from .generic_text import GenericText
|
||||
from ..logger import logger
|
||||
from abc import ABC, abstractmethod
|
||||
from .enums import *
|
||||
from datetime import *
|
||||
|
||||
|
||||
class PyTeXFormatter(FormatterIF, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
input_file: Optional[Path] = None,
|
||||
config: Optional[FormattingConfig] = None,
|
||||
git_version_info: Optional[GitVersionInfo] = None,
|
||||
locate_file_config: bool = True,
|
||||
allow_infile_config: bool = True
|
||||
):
|
||||
super().__init__(
|
||||
input_file=input_file,
|
||||
config=config
|
||||
)
|
||||
self._config: Optional[FormattingConfig] = self._config # for type-hinting
|
||||
self._git_version_info: Optional[GitVersionInfo] = git_version_info
|
||||
self._allow_infile_config: bool = allow_infile_config
|
||||
self._header: Optional[GenericText] = None
|
||||
if locate_file_config:
|
||||
file_config = self.parse_file_config()
|
||||
if allow_infile_config:
|
||||
infile_config = self.parse_infile_config()
|
||||
self._config = \
|
||||
file_config.merge_with(
|
||||
infile_config,
|
||||
strict=True
|
||||
).merge_with(self.config, strict=False)
|
||||
else:
|
||||
self._config = file_config.merge_with(self.config)
|
||||
else:
|
||||
if allow_infile_config:
|
||||
infile_config = self.parse_infile_config()
|
||||
self._config = infile_config.merge_with(self.config)
|
||||
self._output_file: Optional[TextIO] = None # This may change over time in case of multiple output files
|
||||
self._attribute_dict: Optional[Dict] = None
|
||||
|
||||
def parse_file_config(self) -> FormattingConfig:
|
||||
config_file = self.input_file.with_name(self.input_file.name + PYTEX_CONFIG_FILE_EXTENSION)
|
||||
if config_file.exists():
|
||||
try:
|
||||
return FormattingConfig.from_yaml(config_file)
|
||||
except:
|
||||
raise NotImplementedError # Invalid yaml file format
|
||||
else:
|
||||
return FormattingConfig()
|
||||
|
||||
def parse_infile_config(self) -> FormattingConfig:
|
||||
if self._input_file is None:
|
||||
raise NotImplementedError # no file initialised yet
|
||||
with open(self._input_file, "r") as file:
|
||||
line = file.readline()
|
||||
if re.match(self.config.escape_character + INFILE_CONFIG_BEGIN_CONFIG, line):
|
||||
if not line.strip().lstrip('%').strip() == self.config.escape_character + INFILE_CONFIG_BEGIN_CONFIG:
|
||||
logger.warning(
|
||||
"File {file}: Start of infile config invalid."
|
||||
)
|
||||
config = []
|
||||
while True:
|
||||
line = file.readline()
|
||||
if re.match(self.config.escape_character + INFILE_CONFIG_END_CONFIG, line):
|
||||
if not line.strip().lstrip(
|
||||
'%').strip() == self.config.escape_character + INFILE_CONFIG_END_CONFIG:
|
||||
logger.warning(
|
||||
"File {file}: End of infile config invalid."
|
||||
)
|
||||
break
|
||||
if line == '':
|
||||
raise NotImplementedError # No matching end block
|
||||
config.append(line.lstrip('%').rstrip())
|
||||
try:
|
||||
return FormattingConfig.from_yaml('\n'.join(config))
|
||||
except:
|
||||
raise NotImplementedError # Invalid yaml file format
|
||||
else:
|
||||
return FormattingConfig()
|
||||
|
||||
@property
|
||||
def config(self) -> FormattingConfig:
|
||||
if self._config is None:
|
||||
return FormattingConfig()
|
||||
return self._config
|
||||
|
||||
@config.setter
|
||||
def config(self, formatting_config: FormattingConfig):
|
||||
self._config = formatting_config
|
||||
|
||||
@property
|
||||
def git_version_info(self) -> GitVersionInfo:
|
||||
if self._git_version_info is None:
|
||||
return GitVersionInfo()
|
||||
else:
|
||||
return self._git_version_info
|
||||
|
||||
@property
|
||||
def header(self) -> GenericText:
|
||||
if self._header is None:
|
||||
if not (
|
||||
self.config.include_extra_header
|
||||
or self.config.include_time
|
||||
or self.config.include_pytex_version
|
||||
or self.config.include_pytex_info_text
|
||||
or self.config.include_repo_version
|
||||
or self.config.include_repo_info_text
|
||||
):
|
||||
self._header = GenericText()
|
||||
else:
|
||||
self._header = GenericText()
|
||||
# TODO: handle license
|
||||
if self.config.include_extra_header:
|
||||
self._header += self.config.extra_header + ['']
|
||||
if self.config.include_repo_info_text:
|
||||
self._header += self.config.repo_info_text
|
||||
if self.config.include_pytex_info_text:
|
||||
self._header += self.config.pytex_info_text + ['']
|
||||
|
||||
## TODO handle rest
|
||||
|
||||
return self._header
|
||||
|
||||
def _update_attribute_dict(self):
|
||||
self._attribute_dict: Dict[str, str] = {
|
||||
FormatterProperty.author.value: self.config.author,
|
||||
FormatterProperty.shortauthor.value: self.shortauthor,
|
||||
FormatterProperty.date.value: datetime.now().strftime('%Y/%m/%d'),
|
||||
FormatterProperty.year.value: datetime.now().strftime('%Y'),
|
||||
FormatterProperty.raw_name.value: self.raw_name,
|
||||
FormatterProperty.name.value: self.name,
|
||||
FormatterProperty.version.value: self.config.version,
|
||||
FormatterProperty.file_name.value: self.current_file_name,
|
||||
FormatterProperty.source_file_name.value: self._input_file.name,
|
||||
FormatterProperty.repo_version.value: self.git_version_info.repo_version.version,
|
||||
FormatterProperty.repo_branch.value: self.git_version_info.repo_version.branch,
|
||||
FormatterProperty.repo_commit.value: self.git_version_info.repo_version.commit_hash,
|
||||
FormatterProperty.repo_dirty.value: self.git_version_info.repo_version.dirty,
|
||||
FormatterProperty.pytex_version.value: self.git_version_info.pytex_version.version,
|
||||
FormatterProperty.pytex_branch.value: self.git_version_info.pytex_version.branch,
|
||||
FormatterProperty.pytex_commit.value: self.git_version_info.pytex_version.commit_hash,
|
||||
FormatterProperty.pytex_dirty.value: self.git_version_info.pytex_version.dirty,
|
||||
FormatterProperty.tex_type.value: self.config.tex_type.value,
|
||||
FormatterProperty.tex_flavour.value: self.config.tex_flavour.value,
|
||||
FormatterProperty.file_prefix.value: self.file_prefix,
|
||||
FormatterProperty.description.value: self.config.description,
|
||||
FormatterProperty.Tex_type.value: self.config.tex_type.value.capitalize(),
|
||||
FormatterProperty.tex_out_type.value: self.config.tex_out_type.value,
|
||||
}
|
||||
|
||||
@property
|
||||
def attribute_dict(self) -> Dict:
|
||||
if self._attribute_dict is None:
|
||||
self._update_attribute_dict()
|
||||
return self._attribute_dict
|
||||
|
||||
@property
|
||||
def shortauthor(self) -> str:
|
||||
parts = self.config.author.lower().replace('ß', 'ss').split(' ') # TODO: better non-alphanumeric handling
|
||||
if len(parts) == 1:
|
||||
return parts[0]
|
||||
else:
|
||||
return parts[0][0] + parts[-1]
|
||||
|
||||
@property
|
||||
def raw_name(self) -> str:
|
||||
parts = self._input_file.name.split('.', maxsplit=1)
|
||||
if not len(parts) == 2:
|
||||
raise NotImplementedError # invalid file name
|
||||
else:
|
||||
return parts[0]
|
||||
|
||||
@property
|
||||
def file_prefix(self) -> str:
|
||||
if self.config.naming_scheme == NamingScheme.prepend_author:
|
||||
if self.config.tex_flavour == TeXFlavour.LaTeX2e:
|
||||
return self.shortauthor + '@' + self.raw_name
|
||||
elif self.config.tex_flavour == TeXFlavour.LaTeX3:
|
||||
return self.shortauthor + '_' + self.raw_name.replace('-', '_')
|
||||
else:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
if self.config.tex_flavour == TeXFlavour.LaTeX3:
|
||||
return self.raw_name.replace('-', '_')
|
||||
else:
|
||||
return self.raw_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self.config.naming_scheme == NamingScheme.prepend_author:
|
||||
return self.shortauthor + '-' + self.raw_name
|
||||
else:
|
||||
return self.raw_name
|
||||
|
||||
def current_file_name(self):
|
||||
return self._output_file.name
|
||||
|
||||
def make_header(self, **kwargs) -> str:
|
||||
try:
|
||||
return '\n'.join(
|
||||
[
|
||||
'%' * 80,
|
||||
self.header.format(**self.attribute_dict),
|
||||
'%' * 80,
|
||||
''
|
||||
]
|
||||
)
|
||||
except KeyError:
|
||||
raise NotImplementedError
|
71
PyTeX/format/repo_status_info.py
Normal file
71
PyTeX/format/repo_status_info.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from typing import Optional, Dict
|
||||
|
||||
from PyTeX.build.versioning.version_info.constants import *
|
||||
from .config import Config
|
||||
|
||||
|
||||
class RepoStatusInfo(Config):
|
||||
def __init__(self):
|
||||
|
||||
self._dirty: Optional[bool] = None
|
||||
self._commit_hash: Optional[str] = None
|
||||
self._branch: Optional[str] = None
|
||||
self._version: Optional[str] = None
|
||||
|
||||
def set_from_json(self, content: Optional[Dict]):
|
||||
filled_content: Dict = self._fill_keys(content)
|
||||
self._branch = filled_content[JSON_BRANCH]
|
||||
self._dirty = filled_content[JSON_DIRTY]
|
||||
self._commit_hash = filled_content[JSON_COMMIT_HASH]
|
||||
self._version = filled_content[JSON_VERSION]
|
||||
|
||||
def to_json(self) -> Dict:
|
||||
return {
|
||||
JSON_VERSION: self._version,
|
||||
JSON_DIRTY: self._dirty,
|
||||
JSON_COMMIT_HASH: self._commit_hash,
|
||||
JSON_BRANCH: self._branch
|
||||
}
|
||||
|
||||
@property
|
||||
def dirty(self) -> bool:
|
||||
if self._dirty is None:
|
||||
return True
|
||||
else:
|
||||
return self._dirty
|
||||
|
||||
@dirty.setter
|
||||
def dirty(self, dirty: bool):
|
||||
self._dirty = dirty
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
if self._version is None:
|
||||
return DEFAULT_VERSION
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, version: str):
|
||||
self._version = version
|
||||
|
||||
@property
|
||||
def commit_hash(self) -> str:
|
||||
if self._commit_hash is None:
|
||||
return DEFAULT_HASH
|
||||
else:
|
||||
return self._commit_hash
|
||||
|
||||
@commit_hash.setter
|
||||
def commit_hash(self, commit_hash: str):
|
||||
self._commit_hash = commit_hash
|
||||
|
||||
@property
|
||||
def branch(self) -> str:
|
||||
if self._branch is None:
|
||||
return DEFAULT_BRANCH
|
||||
else:
|
||||
return self._branch
|
||||
|
||||
@branch.setter
|
||||
def branch(self, branch: str):
|
||||
self._branch = branch
|
39
PyTeX/format/simple_tex_formatter.py
Normal file
39
PyTeX/format/simple_tex_formatter.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from .formatting_config import FormattingConfig
|
||||
from .tex_formatter import TexFormatter
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple, Dict
|
||||
from .enums import TeXFlavour, TeXType, FormatterProperty
|
||||
|
||||
|
||||
class SimpleTeXFormatter(TexFormatter):
|
||||
@property
|
||||
def output_file(self) -> str:
|
||||
switcher = {
|
||||
TeXType.TeXClass: '.cls',
|
||||
TeXType.TeXPackage: '.sty',
|
||||
}
|
||||
return self.name + switcher[self.config.tex_type]
|
||||
|
||||
@property
|
||||
def future_config(self) -> List[Tuple[str, FormattingConfig]]:
|
||||
return [] # TODO
|
||||
|
||||
@property
|
||||
def dependencies(self) -> List[str]:
|
||||
return [] # sty / cls file does not depend on anything
|
||||
|
||||
@property
|
||||
def output_files(self) -> List[str]:
|
||||
return [self.output_file]
|
||||
|
||||
def _post_process_line(self, line: str) -> str:
|
||||
if self.config.tex_flavour == TeXFlavour.LaTeX2e:
|
||||
raw = line.rstrip(' %\n')
|
||||
return '' if raw == '' else raw + '%'
|
||||
else:
|
||||
return line.rstrip()
|
||||
|
||||
def format_post_header(self) -> None:
|
||||
self._shipout_line(self._get_provides_text(
|
||||
self.config.tex_type.value.capitalize(),
|
||||
))
|
236
PyTeX/format/tex_formatter.py
Normal file
236
PyTeX/format/tex_formatter.py
Normal file
|
@ -0,0 +1,236 @@
|
|||
from pathlib import Path
|
||||
from typing import List, TextIO, Optional, Tuple, Dict
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .formatting_config import FormattingConfig
|
||||
from .macros import Macro
|
||||
from .pytex_formatter import PyTeXFormatter
|
||||
from .enums import *
|
||||
from ..logger import logger
|
||||
from .errors import *
|
||||
|
||||
|
||||
class LineStream:
|
||||
def __init__(self, filename: Path):
|
||||
self._file = filename
|
||||
self._handle = open(filename, 'r')
|
||||
self._cached_lines: List[str] = []
|
||||
self._file_exhausted: bool = False
|
||||
self._line_number: int = 0
|
||||
|
||||
def current_line(self):
|
||||
return self.future_line(0)
|
||||
|
||||
def set_line(self, line):
|
||||
self.set_future_line(0, line)
|
||||
|
||||
def pop_line(self) -> str:
|
||||
self.reserve_lines(1)
|
||||
line = self._cached_lines[0]
|
||||
self._cached_lines = self._cached_lines[1:]
|
||||
return line
|
||||
|
||||
def push_line(self, line):
|
||||
self.push_lines([line])
|
||||
|
||||
def push_lines(self, lines: List[str]):
|
||||
self._cached_lines = lines + self._cached_lines
|
||||
|
||||
def future_line(self, pos: int):
|
||||
self.reserve_lines(pos + 1)
|
||||
return self._cached_lines[pos]
|
||||
|
||||
def set_future_line(self, pos: int, line: str):
|
||||
self.reserve_lines(pos + 1)
|
||||
self._cached_lines[pos] = line
|
||||
|
||||
def reserve_lines(self, num_lines):
|
||||
for i in range(0, num_lines - len(self._cached_lines)):
|
||||
self._cached_lines.append(
|
||||
self._handle.readline()
|
||||
)
|
||||
self._line_number += 1
|
||||
if self._cached_lines[-1] == '':
|
||||
self._handle.close()
|
||||
self._file_exhausted = True
|
||||
|
||||
@property
|
||||
def exhausted(self) -> bool:
|
||||
a = self._file_exhausted
|
||||
b = len(self._cached_lines) == 0
|
||||
return a & b
|
||||
|
||||
@property
|
||||
def line_number(self) -> int:
|
||||
return self._line_number
|
||||
|
||||
|
||||
class TexFormatter(PyTeXFormatter, ABC):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._macros: List[Macro] = []
|
||||
self._line_stream: Optional[LineStream] = None
|
||||
self._mode = FormatterMode.normal
|
||||
|
||||
def open_output_stream(self, build_dir: Path, filename: str) -> None:
|
||||
"""
|
||||
:param build_dir: Where to open output stream
|
||||
:param filename: Name of file
|
||||
"""
|
||||
out_file = build_dir / filename
|
||||
if out_file.exists():
|
||||
raise NotImplementedError
|
||||
else:
|
||||
self._output_file = out_file.open('w')
|
||||
self.mode = FormatterMode.normal
|
||||
|
||||
def close_output_stream(self) -> None:
|
||||
self._output_file.close()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def future_config(self) -> List[Tuple[str, FormattingConfig]]:
|
||||
"""
|
||||
# TODO
|
||||
:return:
|
||||
"""
|
||||
|
||||
@property
|
||||
def line_stream(self) -> LineStream:
|
||||
if self._line_stream is None:
|
||||
self._line_stream = LineStream(self.input_file)
|
||||
return self._line_stream
|
||||
|
||||
@property
|
||||
def macros(self) -> List[Macro]:
|
||||
return self._macros
|
||||
|
||||
@macros.setter
|
||||
def macros(self, macros: List[Macro]):
|
||||
self._macros = macros
|
||||
|
||||
def _handle_macro(self, macro: Macro):
|
||||
try:
|
||||
replacement, shipout = macro.apply(
|
||||
self.line_stream.current_line(),
|
||||
self
|
||||
)
|
||||
except PyTeXMacroError as e:
|
||||
e.add_explanation('While applying macro')
|
||||
raise e
|
||||
if shipout:
|
||||
self.line_stream.pop_line()
|
||||
if isinstance(replacement, str):
|
||||
self._shipout_line(replacement)
|
||||
else:
|
||||
if len(replacement) >= 1:
|
||||
self._shipout_line(replacement[0])
|
||||
self._line_stream.push_lines(replacement[1:])
|
||||
else:
|
||||
if isinstance(replacement, str):
|
||||
self.line_stream.set_line(replacement)
|
||||
else:
|
||||
self.line_stream.pop_line()
|
||||
self.line_stream.push_lines(replacement)
|
||||
|
||||
def _post_process_line(self, line: str) -> str:
|
||||
"""
|
||||
Can strip line or add comment symbols etc.
|
||||
:param line: Line, potentially with trailing newline
|
||||
:return: Line without newline symbol
|
||||
"""
|
||||
return line.rstrip()
|
||||
|
||||
def _shipout_line(self, line) -> None:
|
||||
"""
|
||||
The line might get dropped according to current mode
|
||||
|
||||
:param line: Line to shipout
|
||||
:return: None
|
||||
"""
|
||||
self.write_line(line.rstrip('\n') + '\n')
|
||||
|
||||
def write_line(self, line: str):
|
||||
if self._output_file is None:
|
||||
raise NotImplementedError
|
||||
if not self.mode.is_drop():
|
||||
self._output_file.write(line)
|
||||
|
||||
def format_pre_header(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def mode(self) -> FormatterMode:
|
||||
return self._mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, mode: FormatterMode) -> None:
|
||||
self._mode = mode
|
||||
|
||||
def _get_provides_text(self, provided_type: str) -> str:
|
||||
if self.config.tex_flavour == TeXFlavour.LaTeX2e:
|
||||
return \
|
||||
r'\Provides%s{%s}[%s - %s]' \
|
||||
% (
|
||||
provided_type,
|
||||
self.name,
|
||||
self.attribute_dict[FormatterProperty.date.value],
|
||||
self.attribute_dict[FormatterProperty.description.value]
|
||||
)
|
||||
else:
|
||||
return \
|
||||
'\\ProvidesExpl%s {%s} {%s} {%s}\n {%s}' \
|
||||
% (
|
||||
provided_type,
|
||||
self.name,
|
||||
self.attribute_dict[FormatterProperty.date.value],
|
||||
self.config.version,
|
||||
self.config.description
|
||||
)
|
||||
|
||||
def format_header(self):
|
||||
if self._output_file is None:
|
||||
raise NotImplementedError
|
||||
self._output_file.write(self.make_header())
|
||||
|
||||
def format_post_header(self) -> None:
|
||||
pass
|
||||
|
||||
def format_document(self) -> None:
|
||||
while not self.line_stream.exhausted:
|
||||
try:
|
||||
recent_replacement = True
|
||||
while recent_replacement:
|
||||
recent_replacement = False
|
||||
for macro in self.macros:
|
||||
if macro.matches(self.line_stream.current_line()):
|
||||
try:
|
||||
self._handle_macro(macro)
|
||||
except PyTeXMacroError as e:
|
||||
e.add_explanation('while handling macro')
|
||||
raise e
|
||||
recent_replacement = True
|
||||
break
|
||||
except PyTeXMacroError as e:
|
||||
e.add_explanation(
|
||||
f'in line {self.line_stream.line_number} ({self.line_stream.current_line().rstrip()})'
|
||||
)
|
||||
raise e
|
||||
self._shipout_line(self._post_process_line(
|
||||
self.line_stream.pop_line()
|
||||
))
|
||||
|
||||
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
||||
for filename in self.output_files:
|
||||
try:
|
||||
self.open_output_stream(build_dir, filename)
|
||||
self.format_pre_header()
|
||||
self.format_header()
|
||||
self.format_post_header()
|
||||
self.format_document()
|
||||
self.close_output_stream()
|
||||
except PyTeXError as e:
|
||||
e.add_explanation(f'while formatting output file {filename}')
|
||||
raise e
|
||||
return self.future_config
|
||||
|
1
PyTeX/logger/__init__.py
Normal file
1
PyTeX/logger/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .logger import logger
|
30
PyTeX/logger/add_logging_level.py
Normal file
30
PyTeX/logger/add_logging_level.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# This file is adapted from the haggis library, available at
|
||||
# https://github.com/madphysicist/haggis
|
||||
#
|
||||
# The haggis library is licensed under the
|
||||
# GNU Affero General Public License v3.0 (AGPLv3)
|
||||
#
|
||||
# The code has been adapted since only a small code piece is needed
|
||||
# to avoid introducing an additional install-dependency when
|
||||
# using PyTeX
|
||||
#
|
||||
# You should have received a copy of the AGPLv3 license along with the
|
||||
# PyTeX distribution. If not, refer to
|
||||
# https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def add_logging_level(level_name: str, level_num: int):
|
||||
def log_for_level(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(level_num):
|
||||
self._log(level_num, message, args, **kwargs)
|
||||
|
||||
def log_to_root(message, *args, **kwargs):
|
||||
logging.log(level_num, message, *args, **kwargs)
|
||||
|
||||
logging.addLevelName(level_num, level_name.upper())
|
||||
setattr(logging, level_name.upper(), level_num)
|
||||
setattr(logging.getLoggerClass(), level_name.lower(), log_for_level)
|
||||
setattr(logging, level_name.lower(), log_to_root)
|
15
PyTeX/logger/logger.py
Normal file
15
PyTeX/logger/logger.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import logging
|
||||
from .add_logging_level import add_logging_level
|
||||
|
||||
add_logging_level('VERBOSE', 15)
|
||||
|
||||
formatter = logging.Formatter("[{name}] [{levelname}] {message}", style="{")
|
||||
|
||||
console_logger = logging.StreamHandler()
|
||||
console_logger.setLevel(logging.VERBOSE)
|
||||
console_logger.setFormatter(formatter)
|
||||
|
||||
logger: logging.Logger = logging.getLogger('PyTeX')
|
||||
|
||||
logger.setLevel(logging.VERBOSE)
|
||||
logger.addHandler(console_logger)
|
0
PyTeX/tmp/__init__.py
Normal file
0
PyTeX/tmp/__init__.py
Normal file
81
PyTeX/tmp/generate_properties.py
Normal file
81
PyTeX/tmp/generate_properties.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from typing import Tuple, Union, List, Any
|
||||
|
||||
from PyTeX.format.enums import NamingScheme
|
||||
from PyTeX.format.generic_text import GenericText
|
||||
|
||||
|
||||
def generate_properties(props):
|
||||
props = [x if isinstance(x, Tuple) else (x, None) for x in props]
|
||||
out = []
|
||||
for [prop, default] in props:
|
||||
out.append(
|
||||
" @property\n"
|
||||
" def {prop}(self) -> {type}:\n"
|
||||
" if self._{prop} is None:\n"
|
||||
" return {default}\n"
|
||||
" else:\n"
|
||||
" return self._{prop}\n".format(
|
||||
prop=prop,
|
||||
type=type(default).__name__,
|
||||
default=str(default)
|
||||
)
|
||||
)
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = generate_properties(
|
||||
[
|
||||
("naming_scheme", NamingScheme.prepend_author),
|
||||
("license", []),
|
||||
("include_extra_header", False),
|
||||
("include_build_time", False),
|
||||
("include_pytex_version", False),
|
||||
("include_pytex_info_text", False),
|
||||
("include_repo_version", False),
|
||||
("include_repo_info_text", False),
|
||||
("extra_header", []),
|
||||
("author", "MISSING AUTHOR"),
|
||||
("licenses", GenericText([])),
|
||||
("version", "0.0.0"),
|
||||
("extra_header_file", GenericText([])),
|
||||
("pytex_version", "0.0.0"),
|
||||
("extra_header_file", GenericText([])),
|
||||
"pytex_version",
|
||||
("pytex_info_text", GenericText([])),
|
||||
"repo_version",
|
||||
("repo_info_text", GenericText([])),
|
||||
("include_drv", False),
|
||||
("include_ins", False),
|
||||
("use_docstrip_guards", [])
|
||||
]
|
||||
)
|
||||
out2 = generate_properties(
|
||||
[
|
||||
("recursive", True),
|
||||
("overwrite_existing_files", False),
|
||||
("clean_old_files", False),
|
||||
("allow_dirty", False)
|
||||
]
|
||||
)
|
||||
print(out2)
|
||||
|
||||
|
||||
def generate_properties(attributes: List[Union[str, Tuple[str, Any]]]):
|
||||
attributes = [
|
||||
x if isinstance(x, Tuple) else (x, None) for x in attributes
|
||||
]
|
||||
|
||||
def decorator(cls):
|
||||
for [attribute, default_value] in attributes:
|
||||
def get_attr(self, attribute=attribute, default_value=default_value):
|
||||
if getattr(self, "_" + attribute) is not None:
|
||||
return getattr(self, "_" + attribute)
|
||||
else:
|
||||
return default_value
|
||||
|
||||
prop = property(get_attr)
|
||||
setattr(cls, attribute, prop)
|
||||
return cls
|
||||
|
||||
return decorator
|
72
PyTeX/tmp/pytex_path.py
Normal file
72
PyTeX/tmp/pytex_path.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
import os
|
||||
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
|
||||
|
||||
from PyTeX.build.build import PyTeXBuilder
|
||||
from PyTeX.build.build.enums import PyTeXRootDirType
|
||||
|
||||
|
||||
class PyTeXPurePath:
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXPurePath(PurePath):
|
||||
def __new__(cls, pytex_root_dir_type: PyTeXRootDirType, *args):
|
||||
if cls is PyTeXPurePath:
|
||||
cls = PyTeXPureWindowsPath if os.name == 'nt' else PyTeXPurePosixPath
|
||||
self = cls._from_parts(args)
|
||||
self.__init(pytex_root_dir_type)
|
||||
return self
|
||||
|
||||
def __init(self, pytex_root_dir_type: PyTeXRootDirType):
|
||||
self._pytex_root_dir_type: PyTeXRootDirType = pytex_root_dir_type
|
||||
self._root_dir: Path = {
|
||||
PyTeXRootDirType.BUILD: PyTeXBuilder.build_root(),
|
||||
PyTeXRootDirType.PYTEX_SOURCE: PyTeXBuilder.source_root(),
|
||||
PyTeXRootDirType.DOC: PyTeXBuilder.doc_root(),
|
||||
PyTeXRootDirType.TEX_SOURCE: PyTeXBuilder.tex_root()
|
||||
}[self._pytex_root_dir_type]
|
||||
try:
|
||||
if self._pytex_root_dir_type == PyTeXRootDirType.PYTEX_SOURCE:
|
||||
self._relative_path = self.relative_to(self._root_dir)
|
||||
else:
|
||||
self._relative_path = self.relative_to(
|
||||
self._root_dir / PyTeXBuilder.wrapper_dir()
|
||||
)
|
||||
except ValueError as e:
|
||||
raise NotImplementedError
|
||||
|
||||
def root_dir_type(self) -> PyTeXRootDirType:
|
||||
return self._pytex_root_dir_type
|
||||
|
||||
@property
|
||||
def relative_path(self) -> PyTeXPurePath:
|
||||
return self._relative_path
|
||||
|
||||
def __truediv__(self, other):
|
||||
if self._pytex_root_dir_type == PyTeXRootDirType.PYTEX_SOURCE:
|
||||
return super().__truediv__(other)
|
||||
else:
|
||||
return super().__truediv__(PyTeXBuilder.wrapper_dir()).__truediv__(other)
|
||||
|
||||
|
||||
class PyTeXPurePosixPath(PurePosixPath, PyTeXPurePath):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXPureWindowsPath(PureWindowsPath, PyTeXPurePath):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXPath(Path, PyTeXPurePath):
|
||||
def __new__(cls, pytex_root_dir_type: PyTeXRootDirType, *args, **kwargs):
|
||||
if cls is PyTeXPath:
|
||||
cls = PyTeXWindowsPath if os.name == 'nt' else PyTeXPosixPath
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class PyTeXPosixPath(PyTeXPath, PyTeXPurePosixPath):
|
||||
pass
|
||||
|
||||
|
||||
class PyTeXWindowsPath(PyTeXPath, PyTeXPureWindowsPath):
|
||||
pass
|
7
PyTeX/tmp/relpath.py
Normal file
7
PyTeX/tmp/relpath.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
self._pytex_root_dir_type: PyTeXRootDirType = pytex_root_dir_type
|
||||
self._root_dir: Path = {
|
||||
PyTeXRootDirType.BUILD: build_dir_spec.build_root,
|
||||
PyTeXRootDirType.PYTEX_SOURCE: build_dir_spec.pytex_source_root,
|
||||
PyTeXRootDirType.DOC: build_dir_spec.doc_root,
|
||||
PyTeXRootDirType.TEX_SOURCE: build_dir_spec.tex_source_root
|
||||
}[self._pytex_root_dir_type]
|
33
README.md
33
README.md
|
@ -1,32 +1,7 @@
|
|||
# PyTeX
|
||||
# dev
|
||||
|
||||
Some hacky python scripts to simplify my LaTeX package writing.
|
||||
This is the development branch for a fully new re-implementation of PyTeX.
|
||||
|
||||
## Usage
|
||||
Numeruous features have not been implemented, nothing has been tested yet.
|
||||
|
||||
Write packages in `.pytex` format. The `PackageFormatter` class will then - given author and name of the package - read in and format the file to produce a ready to use LaTeX package `.sty` file.
|
||||
|
||||
As an example, see the [LatexPackages](https://github.com/kesslermaximilian/LatexPackages) repository where this is used.
|
||||
|
||||
## Macros
|
||||
Here is a (possibly incomplete) list of the PyTeX macros currently supported. The examples assume that we create a package called `example.sty`, written by myself:
|
||||
|
||||
| macro name | explanation | example |
|
||||
---|---|---
|
||||
`__HEADER__(< package description>)` | inserts package header, including license and LaTeX package header | `\NeedsTexFormat{LaTeX2e}`<br/>`\ProvidesPackage{mkessler-example}[2021/10/07 - <description>]`
|
||||
`__PACKAGE_NAME__` | inserts package name | `mkessler-example`
|
||||
`__PACKAGE_PREFIX__` | inserts package prefix | `mkessler@example@`
|
||||
`__PACKAGE_MACRO__(<macro-name>)`| declares internal package macro | `\mkessler@example@<macro-name>`
|
||||
`__FILE_NAME__`| inserts package file name | `mkessler-example.sty`
|
||||
`__AUTHOR__`| inserts package author | `Maximilian Keßler`
|
||||
`__DATE__`| inserts compilation date in format `%Y/%m/%d` | `2021/10/07`
|
||||
`__NEW_IF__(<name>,<value>)`| declares a new LaTeX if | `\ifmkessler@example@<name>\mkessler@example@<name><value>`
|
||||
`__SET_IF__(<name>,<value>)`| sets the value of a LaTeX if | `\mkessler@example@<name><value>`
|
||||
`__IF__(<name>)`| starts conditional | `\ifmkessler@example@<name>`
|
||||
`__LANGUAGE_OPTIONS__`| inserts default language options | `\newif\mkessler@example@english\mkessler@example@englishtrue`<br/>`\DeclareOption{german}{\mkessler@example@englishfalse}`<br/>`\DeclareOption{ngerman}{\mkessler@example@englishfalse}`<br/>`\DeclareOption{english}{\mkessler@example@englishtrue}`
|
||||
`__LANGUAGE_OPTIONS_X__`| inserts default language options with `xkeyval` | `\newif\mkessler@example@english\mkessler@example@englishtrue`<br/>`\DeclareOptionX{german}{\mkessler@example@englishfalse}`<br/>`\DeclareOptionX{ngerman}{\mkessler@example@englishfalse}`<br/>`\DeclareOptionX{english}{\mkessler@example@englishtrue}`
|
||||
`__END_OPTIONS__`| process options and handle wrong options | `\DeclareOption*{\PackageWarning{mkessler-example}{Unknown '\CurrentOption'}`<br/>`\ProcessOptions\relax`
|
||||
`__END_OPTIONS_X__`| process options with `xkeyval` | `\DeclareOptionX*{\PackageWarning{mkessler-example}{Unknown '\CurrentOption'}`<br/>`\ProcessOptionsX\relax`
|
||||
`__ERROR__(<message>)` | output package error | `\PackageError{mkessler-example}{<message>}`
|
||||
`__WARNING__(<message>)`| output package warning | `\PackageWarning{mkessler-example}{<message>}`
|
||||
`__INFO__(<message>)`| output package info | `\PackageInfo{mkessler-example}{<message>}`
|
||||
The interface might change at any time without further notice.
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
from PyTeX.default_formatters import ClassFormatter, PackageFormatter, DictionaryFormatter
|
||||
|
||||
__all__ = [
|
||||
"ClassFormatter",
|
||||
"PackageFormatter",
|
||||
"DictionaryFormatter"
|
||||
]
|
|
@ -1,9 +0,0 @@
|
|||
from .enums import Attributes, Args
|
||||
|
||||
__all__ = [
|
||||
'LICENSE',
|
||||
'PACKAGE_INFO_TEXT',
|
||||
'PYTEX_INFO_TEXT',
|
||||
'Args',
|
||||
'Attributes'
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Attributes(Enum):
|
||||
name_raw = 'name_raw'
|
||||
author = 'author'
|
||||
author_acronym = 'author_acronym'
|
||||
name_lowercase = 'name_lowercase'
|
||||
prefix = 'prefix'
|
||||
file_name = 'file_name'
|
||||
date = 'date'
|
||||
year = 'year'
|
||||
source_file_name = 'source_file_name'
|
||||
version = 'version'
|
||||
|
||||
|
||||
class Args(Enum):
|
||||
one = 0
|
||||
two = 1
|
||||
three = 2
|
||||
four = 3
|
||||
five = 4
|
|
@ -1,7 +0,0 @@
|
|||
from .build import build
|
||||
from .build_parser import parse_and_build
|
||||
|
||||
__all__ = [
|
||||
'build',
|
||||
'parse_and_build'
|
||||
]
|
152
build/build.py
152
build/build.py
|
@ -1,152 +0,0 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import git
|
||||
|
||||
from PyTeX.config.constants import BUILD_INFO_FILENAME
|
||||
from PyTeX.errors import *
|
||||
|
||||
from .utils import BuildInfo, pytex_msg, TexFileToFormat
|
||||
|
||||
|
||||
def build(
|
||||
src_dir: Optional[Path] = None,
|
||||
build_dir: Optional[Path] = None,
|
||||
input_file: Optional[Path] = None,
|
||||
author: Optional[str] = None,
|
||||
latex_name: str = 'prepend-author', # name handling
|
||||
recursive: bool = False, # input control
|
||||
include_timestamp: bool = False, # header
|
||||
include_pytex_version: bool = False, # header
|
||||
include_license: bool = False, # header
|
||||
include_git_version: bool = False, # header
|
||||
include_pytex_info_text: bool = False, # header
|
||||
extra_header: Optional[Path] = None,
|
||||
allow_dirty: bool = False, # versioning
|
||||
overwrite_existing_files: bool = False, # output control
|
||||
build_all: bool = False, # output control / versioning
|
||||
write_build_information: bool = True, # meta
|
||||
clean_old_files: bool = False
|
||||
):
|
||||
pytex_msg('Getting git repository information...')
|
||||
if extra_header:
|
||||
if extra_header.exists():
|
||||
with open(extra_header, 'r') as f:
|
||||
text = f.readlines()
|
||||
extra_header = [line.rstrip() for line in text]
|
||||
else:
|
||||
raise ExtraHeaderFileNotFoundError
|
||||
current_build_info = BuildInfo(
|
||||
include_timestamp=include_timestamp,
|
||||
include_pytex_version=include_pytex_version,
|
||||
include_license=include_license,
|
||||
include_git_version=include_git_version,
|
||||
include_pytex_info_text=include_pytex_info_text,
|
||||
extra_header=extra_header,
|
||||
author=author,
|
||||
pytex_repo=git.Repo(__file__, search_parent_directories=True),
|
||||
packages_repo=git.Repo(src_dir, search_parent_directories=True)
|
||||
)
|
||||
print(r'[PyTeX] This is PyTex version {pytex_version}'.format(
|
||||
pytex_version=current_build_info.pytex_version
|
||||
))
|
||||
input_dir = src_dir if src_dir else input_file.parent
|
||||
output_dir = build_dir if build_dir else input_file.parent
|
||||
|
||||
last_build_info_file = output_dir / BUILD_INFO_FILENAME
|
||||
if last_build_info_file.exists():
|
||||
with open(output_dir / 'build_info.json', 'r') as f:
|
||||
last_build_info = json.load(f)
|
||||
else:
|
||||
last_build_info = None
|
||||
|
||||
files = []
|
||||
if input_file:
|
||||
files.append(input_file)
|
||||
if src_dir:
|
||||
if recursive:
|
||||
for file in src_dir.rglob('*.pysty'):
|
||||
files.append(file)
|
||||
for file in src_dir.rglob('*.pycls'):
|
||||
files.append(file)
|
||||
for file in src_dir.rglob('*.pydict'):
|
||||
files.append(file)
|
||||
for file in src_dir.rglob('*.pysty3'):
|
||||
files.append(file)
|
||||
for file in src_dir.rglob('*.pycls3'):
|
||||
files.append(file)
|
||||
else:
|
||||
for file in src_dir.glob('*.pysty'):
|
||||
files.append(file)
|
||||
for file in src_dir.glob('*.pycls'):
|
||||
files.append(file)
|
||||
for file in src_dir.glob('*.pydict'):
|
||||
files.append(file)
|
||||
for file in src_dir.glob('*.pysty3'):
|
||||
files.append(file)
|
||||
for file in src_dir.glob('*.pycls3'):
|
||||
files.append(file)
|
||||
|
||||
sources_to_build = []
|
||||
for file in files:
|
||||
if last_build_info:
|
||||
last_build_info_for_this_file =\
|
||||
list(filter(lambda i: i['source file'] == str(file.relative_to(src_dir)), last_build_info['tex_sources']))
|
||||
else:
|
||||
last_build_info_for_this_file = []
|
||||
sources_to_build.append(
|
||||
TexFileToFormat(
|
||||
src_path=file,
|
||||
build_root=output_dir,
|
||||
src_root=src_dir,
|
||||
latex_name=latex_name,
|
||||
current_build_info=current_build_info,
|
||||
last_build_info=last_build_info_for_this_file,
|
||||
allow_dirty=allow_dirty,
|
||||
overwrite_existing_files=overwrite_existing_files,
|
||||
build_all=build_all
|
||||
))
|
||||
|
||||
info_dict = {
|
||||
'build_time': '',
|
||||
'source files': {
|
||||
'version': current_build_info.packages_version,
|
||||
'commit': current_build_info.packages_hash,
|
||||
'dirty': current_build_info.package_repo.is_dirty(untracked_files=True)
|
||||
},
|
||||
'pytex': {
|
||||
'version': current_build_info.pytex_version,
|
||||
'commit': current_build_info.pytex_hash,
|
||||
'dirty': current_build_info.pytex_repo.is_dirty(untracked_files=True)
|
||||
},
|
||||
'tex_sources': [
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
for source in sources_to_build:
|
||||
infos = source.format()
|
||||
for info in infos:
|
||||
info_dict['tex_sources'].append(info)
|
||||
|
||||
built_files = [info['name'] for info in info_dict['tex_sources']]
|
||||
if last_build_info:
|
||||
lastly_built_files = [info['name'] for info in last_build_info['tex_sources']]
|
||||
else:
|
||||
lastly_built_files = []
|
||||
if clean_old_files:
|
||||
for file in output_dir.rglob('*'):
|
||||
if str(file.relative_to(output_dir)) in lastly_built_files:
|
||||
if str(file.relative_to(output_dir)) not in built_files:
|
||||
print(f'[PyTeX] Removing old built file {str(file.relative_to(output_dir))}')
|
||||
file.unlink()
|
||||
elif not str(file.relative_to(output_dir)) in built_files:
|
||||
if not file.is_dir() and not str(file.relative_to(output_dir)) == 'build_info.json':
|
||||
# PyTeX does not at all know something about this file
|
||||
raise UnknownFileInBuildDirectory(file.relative_to(output_dir))
|
||||
|
||||
if write_build_information:
|
||||
with open(output_dir / 'build_info.json', 'w') as f:
|
||||
json.dump(info_dict, f, indent=4)
|
||||
pytex_msg('Build done')
|
|
@ -1,123 +0,0 @@
|
|||
import argparse
|
||||
import pathlib
|
||||
|
||||
from PyTeX.config import FILENAME_TYPE_PREPEND_AUTHOR, FILENAME_TYPE_RAW_NAME
|
||||
from PyTeX.errors import PyTexError
|
||||
|
||||
from .build import build
|
||||
|
||||
|
||||
def parse_and_build(arglist: [str]):
|
||||
parser = argparse.ArgumentParser(description='Incrementally build LatexPackages with PyTeX')
|
||||
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||
input_group.add_argument(
|
||||
'-s', '--source-dir',
|
||||
metavar='SRC_DIR',
|
||||
help='Relative or absolute path to source directory of .pysty or .pycls files',
|
||||
type=pathlib.Path,
|
||||
nargs='?',
|
||||
default='./src',
|
||||
dest='src_dir'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-b', '--build-dir',
|
||||
metavar='BUILD_DIR',
|
||||
help='Relativ or absolute path to output directory for processed packages and classes',
|
||||
type=pathlib.Path,
|
||||
nargs='?',
|
||||
default='./build',
|
||||
dest='build_dir'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--recursive',
|
||||
help='Recursively search subdirectories for files. Default: false',
|
||||
action='store_true',
|
||||
dest='recursive'
|
||||
)
|
||||
input_group.add_argument(
|
||||
'-i', '--input-file',
|
||||
metavar='FILE',
|
||||
help='Filename to be built. Can be in valid .pysty or .pycls format',
|
||||
type=pathlib.Path,
|
||||
dest='input_file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--name',
|
||||
help='Name of the package / class to be formatted.',
|
||||
type=str,
|
||||
choices=[FILENAME_TYPE_RAW_NAME, FILENAME_TYPE_PREPEND_AUTHOR],
|
||||
default=FILENAME_TYPE_PREPEND_AUTHOR,
|
||||
dest='latex_name'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-g', '--git-version',
|
||||
help='Insert git version information into build. This assumes your input'
|
||||
'files are located in a git repository. Default: false',
|
||||
action='store_true',
|
||||
dest='include_git_version'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d', '--allow-dirty',
|
||||
help='If git flag is set, allow building of a dirty repo. Default: false',
|
||||
action='store_true',
|
||||
dest='allow_dirty'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p',
|
||||
'--pytex-version',
|
||||
help='Write PyTeX version information into built LaTeX files',
|
||||
action='store_true',
|
||||
dest='include_pytex_version'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--build-time',
|
||||
help='Insert build time into built LaTeX files',
|
||||
action='store_true',
|
||||
dest='include_timestamp'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l', '--license',
|
||||
help='Insert MIT license into package header',
|
||||
action='store_true',
|
||||
dest='include_license'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--author',
|
||||
help='Set author of packages',
|
||||
type=str,
|
||||
dest='author'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--force',
|
||||
help='Overwrite unknown existing files without confirmation',
|
||||
action='store_true',
|
||||
dest='overwrite_existing_files'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pytex-info-text',
|
||||
help='Include a PyTeX info text into headers',
|
||||
action='store_true',
|
||||
dest='include_pytex_info_text'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e', '--extra-header',
|
||||
help='Path to file containing extra text for header of each package',
|
||||
type=pathlib.Path,
|
||||
dest='extra_header'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--clean-old-files',
|
||||
help='Cleans old files present in build order that are not present in the sources anymore. '
|
||||
'Setting this option guarantees that the build directory will be equivalent as if a '
|
||||
'clean new build has been made (Build metadata might differ).',
|
||||
action='store_true'
|
||||
)
|
||||
args = vars(parser.parse_args(arglist))
|
||||
for arg in args.keys():
|
||||
if type(args[arg]) == pathlib.PosixPath:
|
||||
args[arg] = args[arg].resolve()
|
||||
try:
|
||||
build(**args)
|
||||
except PyTexError as e:
|
||||
print(e)
|
||||
exit(1)
|
|
@ -1,9 +0,0 @@
|
|||
from .git_version import git_describe, get_history, get_latest_commit
|
||||
from .recent import is_recent
|
||||
|
||||
__all__ = [
|
||||
'git_describe',
|
||||
'get_history',
|
||||
'get_latest_commit',
|
||||
'is_recent'
|
||||
]
|
|
@ -1,53 +0,0 @@
|
|||
import git
|
||||
from typing import Dict
|
||||
|
||||
|
||||
def get_latest_commit(repo):
|
||||
if repo.head.is_detached:
|
||||
return repo.head.commit
|
||||
else:
|
||||
return repo.head.ref.commit
|
||||
|
||||
|
||||
def get_history(commit: git.objects.commit.Commit, priority=0, depth=0) -> Dict:
|
||||
commit_history = {commit.hexsha: {
|
||||
'priority': priority,
|
||||
'depth': depth
|
||||
}}
|
||||
try:
|
||||
if len(commit.parents) > 0:
|
||||
commit_history.update(get_history(commit.parents[0], priority, depth + 1))
|
||||
for parent in commit.parents[1:]:
|
||||
commit_history.update(get_history(parent, priority + 1, depth + 1))
|
||||
except ValueError:
|
||||
pass
|
||||
return commit_history
|
||||
|
||||
|
||||
def git_describe(commit: git.objects.commit.Commit):
|
||||
commit_history = get_history(commit)
|
||||
latest_tag = None
|
||||
for tag in commit.repo.tags:
|
||||
sha = tag.commit.hexsha
|
||||
if sha in commit_history.keys():
|
||||
if latest_tag is None:
|
||||
latest_tag = tag
|
||||
elif commit_history[sha]['priority'] < commit_history[latest_tag.commit.hexsha]['priority']:
|
||||
latest_tag = tag
|
||||
elif commit_history[sha]['priority'] > commit_history[latest_tag.commit.hexsha]['priority']:
|
||||
pass # move on
|
||||
elif commit_history[sha]['depth'] < commit_history[latest_tag.commit.hexsha]['depth']:
|
||||
latest_tag = tag
|
||||
elif commit_history[sha]['depth'] > commit_history[latest_tag.commit.hexsha]['depth']:
|
||||
pass # move on
|
||||
elif tag.object.tagged_date > latest_tag.object.tagged_date:
|
||||
latest_tag = tag
|
||||
if latest_tag is None:
|
||||
return "No tags found - cannot describe anything."
|
||||
else:
|
||||
msg = latest_tag.name
|
||||
if commit_history[latest_tag.commit.hexsha]['depth'] != 0:
|
||||
msg += "-{depth}".format(depth=commit_history[latest_tag.commit.hexsha]['depth'])
|
||||
if commit.repo.is_dirty(untracked_files=True):
|
||||
msg += '-*'
|
||||
return msg
|
|
@ -1,9 +0,0 @@
|
|||
from .build_information import BuildInfo
|
||||
from .pytex_file import TexFileToFormat
|
||||
from .pytex_msg import pytex_msg
|
||||
|
||||
__all__ = [
|
||||
'BuildInfo',
|
||||
'TexFileToFormat',
|
||||
'pytex_msg'
|
||||
]
|
|
@ -1,137 +0,0 @@
|
|||
import git
|
||||
import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from PyTeX.build.git_hook import get_latest_commit
|
||||
from PyTeX.config.header_parts import *
|
||||
|
||||
|
||||
class BuildInfo:
|
||||
def __init__(
|
||||
self,
|
||||
include_timestamp: bool = False,
|
||||
include_pytex_version: bool = False,
|
||||
include_license: bool = False,
|
||||
include_git_version: bool = False,
|
||||
include_pytex_info_text: bool = False,
|
||||
extra_header: Optional[List[str]] = None,
|
||||
author: Optional[str] = None,
|
||||
pytex_repo: Optional[git.Repo] = None,
|
||||
packages_repo: Optional[git.Repo] = None):
|
||||
|
||||
self.author = author
|
||||
self.build_time = datetime.datetime.now().strftime('%Y/%m/%d %H:%M')
|
||||
|
||||
self._pytex_repo = pytex_repo
|
||||
self._packages_repo = packages_repo
|
||||
self._pytex_repo_commit = None
|
||||
self._packages_repo_commit = None
|
||||
self._pytex_repo_version = None
|
||||
self._packages_repo_version = None
|
||||
|
||||
self._header = None
|
||||
|
||||
self.get_repo_commits()
|
||||
self.get_repo_version()
|
||||
|
||||
self.create_header(
|
||||
include_license=include_license,
|
||||
include_pytex_info_text=include_pytex_info_text,
|
||||
include_timestamp=include_timestamp,
|
||||
include_git_version=include_git_version,
|
||||
include_pytex_version=include_pytex_version,
|
||||
extra_header=extra_header
|
||||
)
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return self._header
|
||||
|
||||
@property
|
||||
def pytex_version(self):
|
||||
return self._pytex_repo_version
|
||||
|
||||
@property
|
||||
def packages_version(self):
|
||||
return self._packages_repo_version
|
||||
|
||||
@property
|
||||
def pytex_hash(self):
|
||||
return self._pytex_repo_commit.hexsha
|
||||
|
||||
@property
|
||||
def packages_hash(self):
|
||||
return self._packages_repo_commit.hexsha
|
||||
|
||||
@property
|
||||
def package_repo(self):
|
||||
return self._packages_repo
|
||||
|
||||
@property
|
||||
def pytex_repo(self):
|
||||
return self._pytex_repo
|
||||
|
||||
def get_repo_commits(self):
|
||||
if self._packages_repo:
|
||||
self._packages_repo_commit = get_latest_commit(self._packages_repo)
|
||||
if self._pytex_repo:
|
||||
self._pytex_repo_commit = get_latest_commit(self._pytex_repo)
|
||||
|
||||
def get_repo_version(self):
|
||||
if self._packages_repo_commit:
|
||||
self._packages_repo_version = self._packages_repo.git.describe()
|
||||
if self._pytex_repo_commit:
|
||||
self._pytex_repo_version = self._pytex_repo.git.describe()
|
||||
|
||||
def create_header(
|
||||
self,
|
||||
include_timestamp: bool = False,
|
||||
include_pytex_version: bool = False,
|
||||
include_license: bool = False,
|
||||
include_git_version: bool = False,
|
||||
include_pytex_info_text: bool = False,
|
||||
extra_header: Optional[List[str]] = None
|
||||
):
|
||||
if not (include_license
|
||||
or include_pytex_info_text
|
||||
or include_timestamp
|
||||
or include_pytex_version
|
||||
or include_git_version
|
||||
or extra_header):
|
||||
self._header = None
|
||||
return
|
||||
else:
|
||||
self._header = []
|
||||
if include_license:
|
||||
self._header += LICENSE + ['']
|
||||
if include_pytex_info_text:
|
||||
self._header += PYTEX_INFO_TEXT + ['']
|
||||
if include_timestamp or include_pytex_version or include_git_version:
|
||||
self._header += BUILD_DETAILS
|
||||
if include_timestamp:
|
||||
self._header += BUILD_TIME
|
||||
if include_pytex_version:
|
||||
self._header += PYTEX_VERSION
|
||||
if include_git_version:
|
||||
self._header += SOURCE_CODE_VERSION
|
||||
if len(self._header) > 0:
|
||||
self._header += ['']
|
||||
if extra_header:
|
||||
self._header += extra_header + ['']
|
||||
|
||||
if self._header[-1] == '':
|
||||
self._header.pop()
|
||||
|
||||
formatted_header = []
|
||||
for line in self._header:
|
||||
formatted_header.append(line.format(
|
||||
year=datetime.datetime.now().strftime('%Y'),
|
||||
copyright_holders=self.author,
|
||||
source_file='{source_file}',
|
||||
latex_file_type='{latex_file_type}',
|
||||
pytex_version=self.pytex_version,
|
||||
pytex_commit_hash=self.pytex_hash[:7],
|
||||
packages_version=self.packages_version,
|
||||
packages_commit_hash=self.packages_hash[:7]
|
||||
))
|
||||
self._header = formatted_header
|
|
@ -1,152 +0,0 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
from PyTeX.build.git_hook import is_recent, get_latest_commit
|
||||
from PyTeX import PackageFormatter, ClassFormatter, DictionaryFormatter
|
||||
from PyTeX.errors import *
|
||||
from .pytex_msg import pytex_msg
|
||||
from PyTeX.utils import md5
|
||||
|
||||
from .build_information import BuildInfo
|
||||
|
||||
|
||||
class TexFileToFormat:
|
||||
def __init__(
|
||||
self,
|
||||
src_path: Path,
|
||||
build_root: Path,
|
||||
src_root: Path,
|
||||
latex_name: str,
|
||||
current_build_info: BuildInfo,
|
||||
last_build_info: Optional[List[dict]],
|
||||
allow_dirty: bool = False,
|
||||
overwrite_existing_files: bool = False,
|
||||
build_all: bool = False):
|
||||
self.src_path = src_path
|
||||
self.build_root = build_root
|
||||
self.src_root = src_root
|
||||
self.build_path = build_root / src_path.parent.relative_to(src_root)
|
||||
self.latex_name = latex_name # Still an identifier on how to name the package when being formatted
|
||||
self.current_build_info = current_build_info
|
||||
self.last_build_info_all = last_build_info
|
||||
self.last_build_info = self.last_build_info_all[0] if self.last_build_info_all else None
|
||||
self.allow_dirty = allow_dirty
|
||||
self.overwrite_existing_files: overwrite_existing_files
|
||||
self.build_all = build_all
|
||||
self._header: Optional[List[str]] = None
|
||||
self.__format_header()
|
||||
|
||||
self.dirty = not is_recent(self.src_path, self.current_build_info.package_repo, compare=None)
|
||||
self.pytex_dirty: bool = self.current_build_info.pytex_repo.is_dirty(
|
||||
working_tree=True,
|
||||
untracked_files=True
|
||||
)
|
||||
if self.last_build_info:
|
||||
self.recent: bool = is_recent(
|
||||
file=self.src_path,
|
||||
repo=self.current_build_info.package_repo,
|
||||
compare=self.current_build_info.package_repo.commit(self.last_build_info['source commit hash'])
|
||||
)
|
||||
self.pytex_recent: bool = get_latest_commit(
|
||||
self.current_build_info.pytex_repo
|
||||
).hexsha == self.last_build_info['pytex commit hash']
|
||||
else:
|
||||
self.recent = False
|
||||
self.pytex_recent = False
|
||||
|
||||
def format(self) -> List[dict]:
|
||||
if self.dirty or self.pytex_dirty:
|
||||
if not self.allow_dirty:
|
||||
raise SubmoduleDirtyForbiddenError
|
||||
# TODO: add this to the header...?
|
||||
return self.__format() # Dirty files are always built, since we have no information about them
|
||||
elif self.build_all:
|
||||
return self.__format() # Build file since we build all of them
|
||||
elif not self.pytex_recent or not self.recent:
|
||||
return self.__format() # Build file since either pytex or package repo is not recent
|
||||
elif self.last_build_info and self.last_build_info['dirty']:
|
||||
return self.__format() # Build file since we do not know in what state it is
|
||||
else:
|
||||
return self.last_build_info_all
|
||||
|
||||
def __format_header(self):
|
||||
new_header = []
|
||||
if self.current_build_info.header:
|
||||
for line in self.current_build_info.header:
|
||||
if '.pysty' in self.src_path.name:
|
||||
latex_file_type = 'package'
|
||||
elif '.pycls' in self.src_path.name:
|
||||
latex_file_type = 'class'
|
||||
elif '.pydict' in self.src_path.name:
|
||||
latex_file_type = 'dictionary'
|
||||
else:
|
||||
raise ProgrammingError
|
||||
new_header.append(line.format(
|
||||
source_file=self.src_path.name,
|
||||
latex_file_type=latex_file_type
|
||||
))
|
||||
self._header = new_header
|
||||
|
||||
def __format(self) -> List[dict]:
|
||||
if self.src_path.name.endswith('.pysty'):
|
||||
formatter = PackageFormatter(
|
||||
package_name=self.src_path.with_suffix('').name,
|
||||
author=self.current_build_info.author,
|
||||
extra_header=self._header,
|
||||
tex_version='LaTeX2e',
|
||||
version=self.current_build_info.packages_version,
|
||||
latex_name=self.latex_name)
|
||||
elif self.src_path.name.endswith('.pycls'):
|
||||
formatter = ClassFormatter(
|
||||
class_name=self.src_path.with_suffix('').name,
|
||||
author=self.current_build_info.author,
|
||||
extra_header=self._header,
|
||||
tex_version='LaTeX2e',
|
||||
version=self.current_build_info.packages_version,
|
||||
latex_name=self.latex_name)
|
||||
elif self.src_path.name.endswith('.pysty3'):
|
||||
formatter = PackageFormatter(
|
||||
package_name=self.src_path.with_suffix('').name,
|
||||
author=self.current_build_info.author,
|
||||
extra_header=self._header,
|
||||
tex_version='LaTeX3',
|
||||
version=self.current_build_info.packages_version,
|
||||
latex_name=self.latex_name)
|
||||
elif self.src_path.name.endswith('.pycls3'):
|
||||
formatter = ClassFormatter(
|
||||
class_name=self.src_path.with_suffix('').name,
|
||||
author=self.current_build_info.author,
|
||||
extra_header=self._header,
|
||||
tex_version='LaTeX3',
|
||||
version=self.current_build_info.packages_version,
|
||||
latex_name=self.latex_name)
|
||||
elif self.src_path.name.endswith('.pydict'):
|
||||
formatter = DictionaryFormatter(
|
||||
kind=self.src_path.with_suffix('').name,
|
||||
author=self.current_build_info.author,
|
||||
header=self._header
|
||||
)
|
||||
else:
|
||||
raise ProgrammingError
|
||||
formatter.make_default_macros()
|
||||
written_files = formatter.format_file(
|
||||
input_path=self.src_path,
|
||||
output_dir=self.build_path,
|
||||
relative_name=str(self.src_path.relative_to(self.src_root)),
|
||||
last_build_info=self.last_build_info_all)
|
||||
build_infos = []
|
||||
for written_file in written_files:
|
||||
info = {
|
||||
'name': str(self.src_path.parent.relative_to(self.src_root)) + "/" + written_file,
|
||||
'source file': str(self.src_path.relative_to(self.src_root)),
|
||||
'build time': self.current_build_info.build_time,
|
||||
'source version': self.current_build_info.packages_version,
|
||||
'source commit hash': self.current_build_info.packages_hash,
|
||||
'pytex version': self.current_build_info.pytex_version,
|
||||
'pytex commit hash': self.current_build_info.pytex_hash,
|
||||
'md5sum': md5(self.build_root / self.src_path.parent.relative_to(self.src_root) / written_file),
|
||||
'dirty': self.dirty
|
||||
}
|
||||
build_infos.append(info)
|
||||
pytex_msg('Written file {}'.format(written_file))
|
||||
return build_infos
|
|
@ -1,2 +0,0 @@
|
|||
def pytex_msg(msg: str):
|
||||
print('[PyTeX] ' + msg)
|
|
@ -1,12 +0,0 @@
|
|||
from .constants import FILENAME_TYPE_PREPEND_AUTHOR, FILENAME_TYPE_RAW_NAME, DATE_FORMAT, BUILD_INFO_FILENAME
|
||||
from .header_parts import LICENSE, PYTEX_INFO_TEXT, BUILD_DETAILS
|
||||
|
||||
__all__ = [
|
||||
'FILENAME_TYPE_PREPEND_AUTHOR',
|
||||
'FILENAME_TYPE_RAW_NAME',
|
||||
'DATE_FORMAT',
|
||||
'BUILD_INFO_FILENAME',
|
||||
'LICENSE',
|
||||
'PYTEX_INFO_TEXT',
|
||||
'BUILD_DETAILS'
|
||||
]
|
|
@ -1,4 +0,0 @@
|
|||
FILENAME_TYPE_PREPEND_AUTHOR = 'prepend-author'
|
||||
FILENAME_TYPE_RAW_NAME = 'raw'
|
||||
DATE_FORMAT = '%Y/%m/%d %H:%M'
|
||||
BUILD_INFO_FILENAME = 'build_info.json'
|
|
@ -1,45 +0,0 @@
|
|||
LICENSE = [
|
||||
'Copyright © {year} {copyright_holders}',
|
||||
'',
|
||||
'Permission is hereby granted, free of charge, to any person obtaining a copy',
|
||||
'of this software and associated documentation files (the “Software”), to deal',
|
||||
'in the Software without restriction, including without limitation the rights',
|
||||
'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell',
|
||||
'copies of the Software, and to permit persons to whom the Software is',
|
||||
'furnished to do so, subject to the following conditions:',
|
||||
'The above copyright notice and this permission notice shall be included in all',
|
||||
'copies or substantial portions of the Software.',
|
||||
'',
|
||||
'THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR',
|
||||
'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,',
|
||||
'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE',
|
||||
'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER',
|
||||
'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,',
|
||||
'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE',
|
||||
'SOFTWARE.'
|
||||
]
|
||||
|
||||
PYTEX_INFO_TEXT = [
|
||||
"This {latex_file_type} has been generated by PyTeX, available at",
|
||||
" https://github.com/kesslermaximilian/PyTeX",
|
||||
"and built from source file '{source_file}'.",
|
||||
"It is STRONGLY DISCOURAGED to edit this source file directly, since local",
|
||||
"changes will not be versioned by Git and overwritten by the next build. Always",
|
||||
"edit the source file and build the {latex_file_type} again."
|
||||
]
|
||||
|
||||
BUILD_DETAILS = [
|
||||
"Build details:"
|
||||
]
|
||||
|
||||
BUILD_TIME = [
|
||||
" Build time: {build_time}"
|
||||
]
|
||||
|
||||
PYTEX_VERSION = [
|
||||
" PyTeX version: {pytex_version} (commit {pytex_commit_hash})"
|
||||
]
|
||||
|
||||
SOURCE_CODE_VERSION = [
|
||||
" Source code version: {packages_version} (commit {packages_commit_hash})"
|
||||
]
|
|
@ -1,9 +0,0 @@
|
|||
from .class_formatter import ClassFormatter
|
||||
from .package_formatter import PackageFormatter
|
||||
from .dictionary_formatter import DictionaryFormatter
|
||||
|
||||
__all__ = [
|
||||
'PackageFormatter',
|
||||
'ClassFormatter',
|
||||
'DictionaryFormatter'
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
import PyTeX.formatter
|
||||
import PyTeX.base
|
||||
import PyTeX.macros
|
||||
|
||||
|
||||
class ClassFormatter(PyTeX.formatter.TexFormatter):
|
||||
def __init__(self, class_name: str, author: str, extra_header: [str] = [], tex_version: str = 'LaTeX2e',
|
||||
version: str = '0.0.0', latex_name: str = 'prepend-author'):
|
||||
PyTeX.formatter.TexFormatter.__init__(
|
||||
self,
|
||||
name=class_name,
|
||||
author=author,
|
||||
header=extra_header,
|
||||
file_extension='.cls',
|
||||
tex_version=tex_version,
|
||||
version=version,
|
||||
latex_name=latex_name
|
||||
)
|
||||
self.tex_version = tex_version
|
||||
|
||||
def make_default_macros(self):
|
||||
PyTeX.macros.make_default_macros(self, 'class', tex_version=self.tex_version)
|
|
@ -1,88 +0,0 @@
|
|||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
from datetime import *
|
||||
import csv
|
||||
|
||||
from PyTeX.formatter import Formatter
|
||||
from PyTeX.utils import ensure_file_integrity
|
||||
|
||||
|
||||
class DictionaryFormatter(Formatter):
|
||||
def __init__(self, kind: str, author: str, header: Optional[List[str]]):
|
||||
self.header = header
|
||||
self.kind = kind.lower()
|
||||
self.author = author
|
||||
author_parts = self.author.lower().replace('ß', 'ss').split(' ')
|
||||
self.author_acronym = author_parts[0][0] + author_parts[-1]
|
||||
self.dictname = r'translator-{kind}-dictionary'.format(
|
||||
kind=self.kind
|
||||
)
|
||||
self.name_lowercase = self.dictname + '-{language}'
|
||||
self.file_name = self.name_lowercase + '.dict'
|
||||
self.date = datetime.now().strftime('%Y/%m/%d')
|
||||
self.year = int(datetime.now().strftime('%Y'))
|
||||
self.replace_dict: Dict = {}
|
||||
self.arg_replace_dict: Dict = {}
|
||||
self.source_file_name = "not specified"
|
||||
super().__init__()
|
||||
|
||||
def expected_file_name(self):
|
||||
return self.file_name
|
||||
|
||||
def format_file(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_dir: Path = None,
|
||||
relative_name: Optional[str] = None,
|
||||
last_build_info: Optional[List[Dict]] = None
|
||||
) -> List[str]:
|
||||
self.source_file_name = str(input_path.name)
|
||||
written_files = []
|
||||
|
||||
if self.header:
|
||||
lines = '%' * 80 + '\n' \
|
||||
+ '\n'.join(map(lambda line: '% ' + line, self.header)) \
|
||||
+ '\n' + '%' * 80 + '\n\n'
|
||||
else:
|
||||
lines = []
|
||||
if output_dir is None:
|
||||
output_dir = input_path.parent
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(input_path, newline='') as csvfile:
|
||||
spamreader = csv.reader(csvfile, delimiter=',', quotechar='|')
|
||||
langs = next(spamreader)
|
||||
translations = {}
|
||||
for lang in langs[1:]:
|
||||
translations[lang] = {}
|
||||
for line in spamreader:
|
||||
for n in range(1, len(line)):
|
||||
translations[langs[n]][line[0]] = line[n]
|
||||
|
||||
for lang in langs[1:]:
|
||||
lang_lines = lines
|
||||
lang_lines += r'\ProvidesDictionary{{{dictname}}}{{{lang}}}'.format(
|
||||
dictname=self.dictname,
|
||||
lang=lang
|
||||
)
|
||||
lang_lines += '\n'
|
||||
lang_lines += '\n'
|
||||
for key in translations[lang].keys():
|
||||
if translations[lang][key].strip() != '':
|
||||
lang_lines += r'\providetranslation{{{key}}}{{{translation}}}'.format(
|
||||
key=key.strip(),
|
||||
translation=translations[lang][key].strip()
|
||||
)
|
||||
lang_lines += '\n'
|
||||
ensure_file_integrity(
|
||||
output_dir / self.file_name.format(language=lang),
|
||||
str(Path(relative_name).parent / self.file_name.format(language=lang)),
|
||||
last_build_info
|
||||
)
|
||||
(output_dir / self.file_name.format(language=lang)).write_text(''.join(lang_lines))
|
||||
written_files.append(self.file_name.format(language=lang))
|
||||
return written_files
|
||||
|
||||
def make_default_macros(self):
|
||||
pass
|
|
@ -1,22 +0,0 @@
|
|||
import PyTeX.formatter
|
||||
import PyTeX.base
|
||||
import PyTeX.macros
|
||||
|
||||
|
||||
class PackageFormatter(PyTeX.formatter.TexFormatter):
|
||||
def __init__(self, package_name: str, author: str, extra_header: [str] = [], tex_version: str = 'LaTeX2e',
|
||||
version: str = '0.0.0', latex_name: str = 'prepend-author'):
|
||||
PyTeX.formatter.TexFormatter.__init__(
|
||||
self,
|
||||
name=package_name,
|
||||
author=author,
|
||||
header=extra_header,
|
||||
file_extension='.sty',
|
||||
tex_version=tex_version,
|
||||
version=version,
|
||||
latex_name=latex_name
|
||||
)
|
||||
self.tex_version = tex_version
|
||||
|
||||
def make_default_macros(self):
|
||||
PyTeX.macros.make_default_macros(self, 'package', tex_version=self.tex_version)
|
|
@ -1,14 +0,0 @@
|
|||
from .errors import PyTexError, SubmoduleDirtyForbiddenError, ProgrammingError, ExtraHeaderFileNotFoundError, \
|
||||
UnknownTexVersionError, ModifiedFileInBuildDirectoryError, UnknownFileInBuildDirectoryNoOverwriteError, \
|
||||
UnknownFileInBuildDirectory
|
||||
|
||||
__all__ = [
|
||||
'PyTexError',
|
||||
'SubmoduleDirtyForbiddenError',
|
||||
'ProgrammingError',
|
||||
'ExtraHeaderFileNotFoundError',
|
||||
'UnknownTexVersionError',
|
||||
'ModifiedFileInBuildDirectoryError',
|
||||
'UnknownFileInBuildDirectoryNoOverwriteError',
|
||||
'UnknownFileInBuildDirectory'
|
||||
]
|
|
@ -1,65 +0,0 @@
|
|||
|
||||
class PyTexError(Exception):
|
||||
def __init__(self, message, *args, **kwargs):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return r'{prefix} ERROR: {message}'.format(
|
||||
prefix='[PyTeX]',
|
||||
message=self.message
|
||||
)
|
||||
|
||||
|
||||
class SubmoduleDirtyForbiddenError(PyTexError):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Submodule PyTeX is dirty, but writing dirty files is not allowed. "
|
||||
"Call PyTeX with '--allow-dirty' option, or commit the submodule changes.")
|
||||
|
||||
|
||||
class ExtraHeaderFileNotFoundError(PyTexError):
|
||||
def __init__(self):
|
||||
super().__init__('Path to extra header content is invalid.')
|
||||
|
||||
|
||||
class ProgrammingError(PyTexError):
|
||||
def __init__(self):
|
||||
super().__init__("A FATAL programming error has occurred. Please contact the developer.")
|
||||
|
||||
|
||||
class UnknownTexVersionError(PyTexError):
|
||||
def __init__(self, tex_version: str):
|
||||
super().__init__(
|
||||
f"Unknown TeX version {tex_version}given. Only 'LaTeX2e' and 'LaTeX3' "
|
||||
f"are currently supported"
|
||||
)
|
||||
|
||||
|
||||
class ModifiedFileInBuildDirectoryError(PyTexError):
|
||||
def __init__(self, filename: str):
|
||||
super().__init__(
|
||||
f"File '{filename}' in the build directory has been modified since the last build. "
|
||||
f"Refusing to overwrite a modified file, since you could lose your manual changes. "
|
||||
f"If you are sure you do not need this anymore, delete it manually and build again. "
|
||||
f"Note that for exactly this reason, it is strongly discouraged to edit built files directly."
|
||||
)
|
||||
|
||||
|
||||
class UnknownFileInBuildDirectoryNoOverwriteError(PyTexError):
|
||||
def __init__(self, filename: str):
|
||||
super().__init__(
|
||||
f"Unknown file {filename} in build directory found. "
|
||||
f"PyTeX has no knowledge whether this file has been built by PyTeX. "
|
||||
f"Refusing to overwrite this file, since you could lose your data. "
|
||||
f"If you are sure, this can be got rid of, delete the file manually, "
|
||||
f"and run the build again."
|
||||
)
|
||||
|
||||
|
||||
class UnknownFileInBuildDirectory(PyTexError):
|
||||
def __init__(self, filename):
|
||||
super().__init__(
|
||||
f"Detected unknown file {filename} in build directory."
|
||||
f"PyTeX has no knowledge about this, you should probably"
|
||||
f"remove it."
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
from .tex_formatter import TexFormatter, Formatter
|
||||
|
||||
__all__ = [
|
||||
'TexFormatter',
|
||||
'Formatter'
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Implementation unknown, this is an Interface"""
|
||||
pass
|
||||
|
||||
def make_default_macros(self) -> None:
|
||||
pass
|
||||
|
||||
def format_file(self, input_path: Path, output_dir: Path, last_build_info: Optional[List[Dict]] = None) -> List[str]:
|
||||
pass
|
||||
|
||||
def expected_file_name(self) -> str:
|
||||
pass
|
|
@ -1,139 +0,0 @@
|
|||
import datetime
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
from datetime import *
|
||||
|
||||
from PyTeX.base import Attributes, Args
|
||||
from PyTeX.errors import *
|
||||
from PyTeX.utils import ensure_file_integrity
|
||||
|
||||
from .formatter import Formatter
|
||||
|
||||
|
||||
class TexFormatter(Formatter):
|
||||
def __init__(self, name: str, author: str, header: Optional[List[str]], file_extension: str,
|
||||
tex_version: str, version: str, latex_name: str):
|
||||
|
||||
self.version = version[1:] if version.startswith('v') else version
|
||||
self.header = header
|
||||
self.name_raw = name
|
||||
self.author = author
|
||||
self.latex_name = latex_name
|
||||
author_parts = self.author.lower().replace('ß', 'ss').split(' ')
|
||||
self.author_acronym = author_parts[0][0] + author_parts[-1]
|
||||
self.date = datetime.now().strftime('%Y/%m/%d')
|
||||
self.year = int(datetime.now().strftime('%Y'))
|
||||
self.replace_dict: Dict = {}
|
||||
self.arg_replace_dict: Dict = {}
|
||||
self.source_file_name = "not specified"
|
||||
if self.latex_name == 'prepend-author':
|
||||
self.name_lowercase = r'{prefix}-{name}'.format(prefix=self.author_acronym,
|
||||
name=self.name_raw.lower().strip().replace(' ', '-'))
|
||||
else:
|
||||
self.name_lowercase = self.name_raw.lower().strip().replace(' ', '-')
|
||||
self.file_name = self.name_lowercase + file_extension
|
||||
if tex_version == 'LaTeX2e':
|
||||
self.prefix = self.name_lowercase.replace('-', '@') + '@'
|
||||
elif tex_version == 'LaTeX3':
|
||||
self.prefix = '__' + self.name_lowercase.replace('-', '_') + '_'
|
||||
else:
|
||||
raise UnknownTexVersionError(tex_version)
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def __command_name2keyword(keyword: str):
|
||||
return '__' + keyword.upper().strip().replace(' ', '_') + '__'
|
||||
|
||||
def expected_file_name(self):
|
||||
return self.file_name
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self.file_name
|
||||
|
||||
def __parse_replacement_args(self, match_groups, *user_args, **user_kwargs):
|
||||
new_args = []
|
||||
for arg in user_args:
|
||||
if type(arg) == Attributes:
|
||||
new_args.append(getattr(self, arg.value))
|
||||
elif type(arg) == Args:
|
||||
new_args.append(match_groups[arg.value].strip())
|
||||
elif type(arg) == str:
|
||||
new_args.append(arg.strip())
|
||||
else:
|
||||
new_args += 'ERROR'
|
||||
new_args = tuple(new_args)
|
||||
new_kwargs = {}
|
||||
for kw in user_kwargs:
|
||||
if type(user_kwargs[kw]) == Attributes:
|
||||
new_kwargs[kw] = getattr(self, user_kwargs[kw].value)
|
||||
elif type(user_kwargs[kw]) == Args:
|
||||
new_kwargs[kw] = match_groups[user_kwargs[kw].value].strip()
|
||||
elif type(user_kwargs[kw]) == str:
|
||||
new_kwargs[kw] = user_kwargs[kw]
|
||||
else:
|
||||
new_kwargs[kw] = 'ERROR'
|
||||
return new_args, new_kwargs
|
||||
|
||||
def __format_string(self, contents: str) -> str:
|
||||
for key in self.replace_dict.keys():
|
||||
contents = contents.replace(key, self.replace_dict[key])
|
||||
return contents
|
||||
|
||||
def __format_string_with_arg(self, contents: str) -> str:
|
||||
for command in self.arg_replace_dict.keys():
|
||||
search_regex = re.compile(r'{keyword}\({arguments}(?<!@)\)'.format(
|
||||
keyword=command,
|
||||
arguments=','.join(['(.*?)'] * self.arg_replace_dict[command]['num_args'])
|
||||
))
|
||||
match = re.search(search_regex, contents)
|
||||
while match is not None:
|
||||
format_args, format_kwargs = self.__parse_replacement_args(
|
||||
list(map(lambda group: group.replace('@)', ')'), match.groups())),
|
||||
*self.arg_replace_dict[command]['format_args'],
|
||||
**self.arg_replace_dict[command]['format_kwargs']
|
||||
)
|
||||
contents = contents.replace(
|
||||
match.group(),
|
||||
self.arg_replace_dict[command]['replacement'].format(*format_args, **format_kwargs)
|
||||
)
|
||||
match = re.search(search_regex, contents)
|
||||
return contents
|
||||
|
||||
def add_replacement(self, keyword: str, replacement: str, *args, **kwargs):
|
||||
args, kwargs = self.__parse_replacement_args([], *args, **kwargs)
|
||||
self.replace_dict[self.__command_name2keyword(keyword)] = replacement.format(*args, **kwargs)
|
||||
|
||||
def add_arg_replacement(self, num_args: int, keyword: str, replacement: str, *args, **kwargs):
|
||||
self.arg_replace_dict[self.__command_name2keyword(keyword)] = {
|
||||
'num_args': num_args,
|
||||
'replacement': replacement,
|
||||
'format_args': args,
|
||||
'format_kwargs': kwargs
|
||||
}
|
||||
|
||||
def format_file(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_dir: Path = None,
|
||||
relative_name: Optional[str] = None,
|
||||
last_build_info: Optional[List[Dict]] = None) -> List[str]:
|
||||
self.source_file_name = str(input_path.name)
|
||||
input_file = input_path.open()
|
||||
lines = input_file.readlines()
|
||||
if self.header:
|
||||
newlines = '%' * 80 + '\n' \
|
||||
+ '\n'.join(map(lambda line: '% ' + line, self.header)) \
|
||||
+ '\n' + '%' * 80 + '\n\n'
|
||||
else:
|
||||
newlines = []
|
||||
for line in lines:
|
||||
newlines += self.__format_string_with_arg(self.__format_string(line))
|
||||
if output_dir is None:
|
||||
output_dir = input_path.parent
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
ensure_file_integrity(output_dir / self.file_name, str(Path(relative_name).parent / self.file_name), last_build_info)
|
||||
|
||||
(output_dir / self.file_name).write_text(''.join(newlines))
|
||||
return [str(self.file_name)]
|
|
@ -1,5 +0,0 @@
|
|||
from .default_macros import make_default_macros
|
||||
|
||||
__all__ = [
|
||||
'make_default_macros'
|
||||
]
|
|
@ -1,74 +0,0 @@
|
|||
import PyTeX.formatter
|
||||
import PyTeX.base
|
||||
import PyTeX.config
|
||||
|
||||
|
||||
def make_default_macros(formatter: PyTeX.formatter.TexFormatter, latex_file_type: str, tex_version: str = 'LaTeX2e'):
|
||||
formatter.add_replacement('{Type} name'.format(Type=latex_file_type), '{}', PyTeX.base.Attributes.name_lowercase)
|
||||
formatter.add_replacement('{Type} prefix'.format(Type=latex_file_type), '{}', PyTeX.base.Attributes.prefix)
|
||||
formatter.add_replacement('date', '{}', PyTeX.base.Attributes.date)
|
||||
formatter.add_replacement('author', '{}', PyTeX.base.Attributes.author)
|
||||
formatter.add_replacement('author acronym', '{}', PyTeX.base.Attributes.author_acronym)
|
||||
formatter.add_arg_replacement(1, 'info', r'\{Type}Info{{{name}}}{{{info}}}',
|
||||
name=PyTeX.base.Attributes.name_lowercase,
|
||||
info=PyTeX.base.Args.one, Type=latex_file_type.capitalize())
|
||||
formatter.add_arg_replacement(1, 'warning', r'\{Type}Warning{{{name}}}{{{warning}}}',
|
||||
name=PyTeX.base.Attributes.name_lowercase, warning=PyTeX.base.Args.one,
|
||||
Type=latex_file_type.capitalize())
|
||||
formatter.add_arg_replacement(1, 'error', r'\{Type}Error{{{name}}}{{{error}}}',
|
||||
name=PyTeX.base.Attributes.name_lowercase, error=PyTeX.base.Args.one,
|
||||
Type=latex_file_type.capitalize())
|
||||
formatter.add_replacement('file name', '{name}', name=PyTeX.base.Attributes.file_name)
|
||||
formatter.add_arg_replacement(1, '{Type} macro'.format(Type=latex_file_type), r'\{}{}',
|
||||
PyTeX.base.Attributes.prefix, PyTeX.base.Args.one)
|
||||
|
||||
if tex_version == 'LaTeX3':
|
||||
header = r'\ProvidesExpl{Type}{{{name_lowercase}}}{{{date}}}{{{version}}}{{{description}}}' + '\n\n'
|
||||
formatter.add_arg_replacement(
|
||||
1, 'header',
|
||||
header,
|
||||
name_lowercase=PyTeX.base.Attributes.name_lowercase,
|
||||
date=PyTeX.base.Attributes.date,
|
||||
description=PyTeX.base.Args.one,
|
||||
Type=latex_file_type.capitalize(),
|
||||
version=PyTeX.base.Attributes.version
|
||||
)
|
||||
elif tex_version == 'LaTeX2e':
|
||||
header = '\\NeedsTeXFormat{{LaTeX2e}}\n' \
|
||||
'\\Provides{Type}{{{name_lowercase}}}[{date} - {description}]\n\n'
|
||||
formatter.add_arg_replacement(
|
||||
1, 'header',
|
||||
header,
|
||||
name_lowercase=PyTeX.base.Attributes.name_lowercase,
|
||||
date=PyTeX.base.Attributes.date,
|
||||
description=PyTeX.base.Args.one,
|
||||
Type=latex_file_type.capitalize(),
|
||||
)
|
||||
formatter.add_arg_replacement(2, 'new if', r'\newif\if{prefix}{condition}\{prefix}{condition}{value}',
|
||||
prefix=PyTeX.base.Attributes.prefix, condition=PyTeX.base.Args.one,
|
||||
value=PyTeX.base.Args.two)
|
||||
formatter.add_arg_replacement(2, 'set if', r'\{prefix}{condition}{value}',
|
||||
prefix=PyTeX.base.Attributes.prefix, condition=PyTeX.base.Args.one,
|
||||
value=PyTeX.base.Args.two)
|
||||
formatter.add_arg_replacement(1, 'if', r'\if{prefix}{condition}', prefix=PyTeX.base.Attributes.prefix,
|
||||
condition=PyTeX.base.Args.one)
|
||||
formatter.add_replacement('language options x',
|
||||
r'\newif\if{prefix}english\{prefix}englishtrue' + '\n' +
|
||||
r'\DeclareOptionX{{german}}{{\{prefix}englishfalse}}' + '\n' +
|
||||
r'\DeclareOptionX{{ngerman}}{{\{prefix}englishfalse}}' + '\n' +
|
||||
r'\DeclareOptionX{{english}}{{\{prefix}englishtrue}}',
|
||||
prefix=PyTeX.base.Attributes.prefix)
|
||||
formatter.add_replacement('language options',
|
||||
r'\newif\if{prefix}english\{prefix}englishtrue' + '\n' +
|
||||
r'\DeclareOption{{german}}{{\{prefix}englishfalse}}' + '\n' +
|
||||
r'\DeclareOption{{ngerman}}{{\{prefix}englishfalse}}' + '\n' +
|
||||
r'\DeclareOption{{english}}{{\{prefix}englishtrue}}',
|
||||
prefix=PyTeX.base.Attributes.prefix)
|
||||
formatter.add_replacement('end options x',
|
||||
r"\DeclareOptionX*{{\{Type}Warning{{{name_lowercase}}}"
|
||||
r"{{Unknown '\CurrentOption'}}}}" + '\n' + r'\ProcessOptionsX*\relax' + '\n',
|
||||
name_lowercase=PyTeX.base.Attributes.name_lowercase, Type=latex_file_type.capitalize())
|
||||
formatter.add_replacement('end options',
|
||||
r"\DeclareOption*{{\{Type}Warning{{{name_lowercase}}}"
|
||||
r"{{Unknown '\CurrentOption'}}}}" + '\n' + r'\ProcessOptions\relax' + '\n',
|
||||
name_lowercase=PyTeX.base.Attributes.name_lowercase, Type=latex_file_type.capitalize())
|
34
main.py
Normal file
34
main.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import signal
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PyTeX.build.build import PyTeXBuilder
|
||||
from PyTeX.build.build.pytex_config import PyTeXConfig
|
||||
from PyTeX.format.formatting_config import FormattingConfig
|
||||
|
||||
if Path('.pytex').exists():
|
||||
shutil.rmtree('.pytex')
|
||||
|
||||
|
||||
def interrupt_handler(signum, frame):
|
||||
if Path('.pytex').exists():
|
||||
shutil.rmtree('.pytex')
|
||||
print('Interrupted execution')
|
||||
quit(1)
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, interrupt_handler)
|
||||
|
||||
|
||||
conf_path = Path('.pytexrc')
|
||||
|
||||
builder = PyTeXBuilder(conf_path)
|
||||
|
||||
|
||||
if 'source' in sys.argv:
|
||||
builder.build_tex_sources()
|
||||
if 'tex' in sys.argv:
|
||||
builder.build_tex_files()
|
||||
|
||||
|
||||
pass
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
PyYAML~=6.0
|
||||
GitPython~=3.1
|
|
@ -1,7 +0,0 @@
|
|||
from. checksum import md5
|
||||
from .file_integrity import ensure_file_integrity
|
||||
|
||||
__all__ = [
|
||||
'md5',
|
||||
'ensure_file_integrity'
|
||||
]
|
|
@ -1,12 +0,0 @@
|
|||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
# https://stackoverflow.com/a/3431838/16371376
|
||||
|
||||
|
||||
def md5(file: Path):
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file, "rb") as f:
|
||||
for block in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(block)
|
||||
return hash_md5.hexdigest()
|
|
@ -1,18 +0,0 @@
|
|||
from pathlib import Path
|
||||
from PyTeX.errors import *
|
||||
from .checksum import md5
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
|
||||
def ensure_file_integrity(file: Path, output_file_name: str, build_info: Optional[List[Dict]] = None):
|
||||
if file.exists():
|
||||
if not build_info:
|
||||
raise UnknownFileInBuildDirectoryNoOverwriteError(str(file))
|
||||
found = False
|
||||
for info in build_info:
|
||||
if info['name'] == output_file_name:
|
||||
if not md5(file) == info['md5sum']:
|
||||
raise ModifiedFileInBuildDirectoryError(str(file))
|
||||
found = True
|
||||
if not found:
|
||||
raise UnknownFileInBuildDirectoryNoOverwriteError(str(file))
|
Loading…
Reference in a new issue