Compare commits

...

No commits in common. "master" and "dev" have entirely different histories.
master ... dev

84 changed files with 3767 additions and 1298 deletions

4
.gitignore vendored
View File

@ -1,4 +1,2 @@
**/__pycache__
.idea/*
__pycache__
main.py
test.py

21
LICENSE
View File

@ -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
View File

0
PyTeX/build/__init__.py Normal file
View File

View File

@ -0,0 +1,2 @@
from .build_dir_spec import BuildDirConfig
from .builder import PyTeXBuilder

View 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

View 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

View 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'

View 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]

View 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'

View 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()

View 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

View 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

View 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)

View 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
)

View File

View File

View 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

View File

@ -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

View 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'

View 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

View File

@ -0,0 +1,2 @@
class PyTeXException(Exception):
pass

0
PyTeX/format/__init__.py Normal file
View File

View 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
View 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
View 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}}'
]

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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 []

View 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

View 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

View 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(),
))

View 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
View File

@ -0,0 +1 @@
from .logger import logger

View 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
View 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
View File

View 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
View 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
View 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]

View File

@ -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.

View File

@ -1,7 +0,0 @@
from PyTeX.default_formatters import ClassFormatter, PackageFormatter, DictionaryFormatter
__all__ = [
"ClassFormatter",
"PackageFormatter",
"DictionaryFormatter"
]

View File

@ -1,9 +0,0 @@
from .enums import Attributes, Args
__all__ = [
'LICENSE',
'PACKAGE_INFO_TEXT',
'PYTEX_INFO_TEXT',
'Args',
'Attributes'
]

View File

@ -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

View File

@ -1,7 +0,0 @@
from .build import build
from .build_parser import parse_and_build
__all__ = [
'build',
'parse_and_build'
]

View File

@ -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')

View File

@ -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)

View File

@ -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'
]

View File

@ -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

View File

@ -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'
]

View File

@ -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

View File

@ -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

View File

@ -1,2 +0,0 @@
def pytex_msg(msg: str):
print('[PyTeX] ' + msg)

View File

@ -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'
]

View File

@ -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'

View File

@ -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})"
]

View File

@ -1,9 +0,0 @@
from .class_formatter import ClassFormatter
from .package_formatter import PackageFormatter
from .dictionary_formatter import DictionaryFormatter
__all__ = [
'PackageFormatter',
'ClassFormatter',
'DictionaryFormatter'
]

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -1,14 +0,0 @@
from .errors import PyTexError, SubmoduleDirtyForbiddenError, ProgrammingError, ExtraHeaderFileNotFoundError, \
UnknownTexVersionError, ModifiedFileInBuildDirectoryError, UnknownFileInBuildDirectoryNoOverwriteError, \
UnknownFileInBuildDirectory
__all__ = [
'PyTexError',
'SubmoduleDirtyForbiddenError',
'ProgrammingError',
'ExtraHeaderFileNotFoundError',
'UnknownTexVersionError',
'ModifiedFileInBuildDirectoryError',
'UnknownFileInBuildDirectoryNoOverwriteError',
'UnknownFileInBuildDirectory'
]

View File

@ -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."
)

View File

@ -1,6 +0,0 @@
from .tex_formatter import TexFormatter, Formatter
__all__ = [
'TexFormatter',
'Formatter'
]

View File

@ -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

View File

@ -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)]

View File

@ -1,5 +0,0 @@
from .default_macros import make_default_macros
__all__ = [
'make_default_macros'
]

View File

@ -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
View 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
View File

@ -0,0 +1,2 @@
PyYAML~=6.0
GitPython~=3.1

View File

@ -1,7 +0,0 @@
from. checksum import md5
from .file_integrity import ensure_file_integrity
__all__ = [
'md5',
'ensure_file_integrity'
]

View File

@ -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()

View File

@ -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))