Compare commits

...

44 commits

Author SHA1 Message Date
60488127e5 detect unknown files in build dir 2022-01-13 21:12:35 +01:00
2cad289b32 fix bug 2022-01-13 21:02:47 +01:00
f5a5ad6328 implement option to prune old files in build directory 2022-01-13 20:56:09 +01:00
bfd0f6e01b ensure file integrity when overwriting files also in dictionary case 2022-01-13 20:49:26 +01:00
f8a6f83de9 ensure file integrity for tex formatters 2022-01-13 20:04:18 +01:00
785eb6923f use paths relative to source / build dir and add md5sum into build info 2022-01-13 19:24:07 +01:00
4e7ea6fd14 write relative file names in build info 2022-01-13 15:33:16 +01:00
026e3a6cb3 adapt to multiple output files per input file in build info 2022-01-12 22:30:10 +01:00
1df567da82 add custom PyTeX exceptions to be thrown and catched during build to prevent ugly python error messaged 2022-01-12 21:35:47 +01:00
4a0e095899 remove 'v' prefix in front of version when formatting LaTeX3 files 2022-01-12 21:02:19 +01:00
d1806d970f Merge branch 'master' of gitlab.com:latexci/packages/PyTeX 2022-01-11 21:37:42 +01:00
75a2b137ec make author acronym available 2022-01-11 21:23:13 +01:00
c8deda5af8 add support for latex3 files 2022-01-11 18:55:18 +01:00
23766e8dcd start adding support for formatting latex3 files 2022-01-11 18:39:56 +01:00
494c43e52b Merge branch 'master' into latex-packages 2022-01-11 18:07:42 +01:00
5e077e77d8 correct header when formatting dictionaries 2022-01-09 14:21:50 +01:00
99c984049c add dictionary formatter for translation package 2022-01-09 14:15:28 +01:00
9bf3daa352 use faster git describe method from GitPython instead 2022-01-09 12:27:00 +01:00
e55a6728a9 fix handling extra header correctly 2021-12-17 15:34:59 +01:00
4c5efa4087 fix error when header is empty
catch case of empty header to not crash
2021-12-17 15:26:28 +01:00
315e3647ab handle package naming style
Option --name can now be set to either
    raw or prepend-author
    this controls the derivation of the name
    of the formatted package from the source
    file read in.
2021-12-17 15:09:04 +01:00
357144e9f0 Merge branch 'latex-packages' 2021-10-29 09:39:39 +02:00
c98fd28245 migrate to gitlab 2021-10-24 00:29:25 +02:00
126d420b7a handle header correctly, i.e. do not only expand on macro 2021-10-22 19:29:23 +02:00
12a03e7989 add parser file for building 2021-10-22 14:52:10 +02:00
4c3867e0f4 refactor files into better module structure 2021-10-22 14:45:32 +02:00
de10ca7546 also build when last build of file has been dirty 2021-10-22 13:56:09 +02:00
f0c32260fa fix some bugs 2021-10-22 13:51:31 +02:00
96f2608751 check if PyTeX is dirty, implement incremental build 2021-10-22 13:39:56 +02:00
e6018a89c8 fix mistake with header type 2021-10-22 13:39:29 +02:00
408a154548 pass more attributes to TexFileToFormat class, write dirty / recent information into build file 2021-10-22 12:56:00 +02:00
c24eb9cd25 fix bugs: correctly diff against working tree or other commits now. add documentation for recent method 2021-10-22 12:55:15 +02:00
7b277ad8b6 integrate some more options. forward arguments to file formatter 2021-10-22 11:58:56 +02:00
cf3edf33c4 write and read in build information in json format 2021-10-22 10:57:16 +02:00
b44af744b0 retrieve repositories 2021-10-22 10:12:28 +02:00
90acc2baf7 add build scripts 2021-10-22 10:01:33 +02:00
05f53a9171 fix extension mistake in package formatter class 2021-10-18 15:55:26 +02:00
ba0b49dbfc Merge branch 'latex-packages'
Formatting project correctly as a package
2021-10-18 15:09:21 +02:00
a1bb182eec format project correctly as python module with corresponding submodules 2021-10-18 15:08:58 +02:00
Maximilian Keßler
b1d429e9b0 Merge branch 'master' of https://github.com/kesslermaximilian/PyTeX 2021-10-08 20:02:24 +02:00
Maximilian Keßler
4409c94af5 xkeyval process options: also parse options from document class 2021-10-08 20:01:23 +02:00
Maximilian Keßler
6c0dee2511 add class formatter. fix typo in error message 2021-10-08 16:34:52 +02:00
Maximilian Keßler
9df6bd0513 add formatter class and derive package formatter from it. future: add class formatter 2021-10-08 16:12:48 +02:00
Maximilian Keßler
1da78575c2
Merge pull request from kesslermaximilian/latex-packages
update readme
2021-10-07 14:29:52 +02:00
32 changed files with 1168 additions and 111 deletions

2
.gitignore vendored
View file

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

7
__init__.py Normal file
View file

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

9
base/__init__.py Normal file
View file

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

View file

@ -2,15 +2,16 @@ from enum import Enum
class Attributes(Enum):
package_name_raw = 'package_name_raw'
name_raw = 'name_raw'
author = 'author'
author_acronym = 'author_acronym'
package_name = 'package_name'
package_prefix = 'package_prefix'
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):

7
build/__init__.py Normal file
View file

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

152
build/build.py Normal file
View file

@ -0,0 +1,152 @@
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')

123
build/build_parser.py Normal file
View file

@ -0,0 +1,123 @@
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

@ -0,0 +1,9 @@
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

@ -0,0 +1,53 @@
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

42
build/git_hook/recent.py Normal file
View file

@ -0,0 +1,42 @@
import git
from .git_version import get_latest_commit
from typing import Union, Optional, List
from pathlib import Path
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
:param compare: commit or list of commits to compare to. None stands for 'working tree'
:return: Whether the given file is currently the same as in compared commit
If compare is a commit, checks if the file has changed since given commit, compared to the most recent commit
of the repository
For a list of commits, checks the same, but for all of these commits. In particular, only returns true if the file
is the same in all of the specified commits (and in the most recent of the repository)
If compare is None, compares the file against the corking tree, i.e. if the file has been modified since the last
commit on the repo. This also involves staged files, i.e. modified and staged files will be considered as
'not recent' since changes are not committed
"""
newly_committed_files = []
if type(compare) == git.Commit:
newly_committed_files = [item.a_path for item in get_latest_commit(repo).diff(compare)]
elif compare is None:
com = get_latest_commit(repo)
newly_committed_files = [item.a_path for item in com.diff(None)]
pass
elif type(compare) == list:
for commit in compare:
for item in get_latest_commit(repo).diff(commit):
newly_committed_files.append(item.a_path)
else:
print("Invalid argument type for compare")
return None
if str(file.relative_to(repo.working_dir)) in newly_committed_files:
return False
else:
return True

9
build/utils/__init__.py Normal file
View file

@ -0,0 +1,9 @@
from .build_information import BuildInfo
from .pytex_file import TexFileToFormat
from .pytex_msg import pytex_msg
__all__ = [
'BuildInfo',
'TexFileToFormat',
'pytex_msg'
]

View file

@ -0,0 +1,137 @@
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

152
build/utils/pytex_file.py Normal file
View file

@ -0,0 +1,152 @@
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

2
build/utils/pytex_msg.py Normal file
View file

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

12
config/__init__.py Normal file
View file

@ -0,0 +1,12 @@
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'
]

4
config/constants.py Normal file
View file

@ -0,0 +1,4 @@
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,5 +1,3 @@
DEFAULT_AUTHOR = 'Maximilian Keßler'
LICENSE = [
'Copyright © {year} {copyright_holders}',
'',
@ -21,21 +19,27 @@ LICENSE = [
'SOFTWARE.'
]
PACKAGE_INFO_TEXT = [
"This LaTeX package is free software and distributed under the MIT License. You",
"may use it freely for your purposes. The latest version of the package can be",
"obtained via GitHub under",
" https://github.com/kesslermaximilian/LatexPackages",
"For further information see the url above.",
"Reportings of bugs, suggestions and improvements are welcome, see the README",
"at the Git repository for further information."
]
PYTEX_INFO_TEXT = [
"This package has been generated by PyTeX, available at",
"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 package again."
"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

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

View file

@ -0,0 +1,22 @@
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

@ -0,0 +1,88 @@
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

@ -0,0 +1,22 @@
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)

14
errors/__init__.py Normal file
View file

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

65
errors/errors.py Normal file
View file

@ -0,0 +1,65 @@
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."
)

6
formatter/__init__.py Normal file
View file

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

17
formatter/formatter.py Normal file
View file

@ -0,0 +1,17 @@
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,34 +1,58 @@
import datetime
import re
from pathlib import Path
from typing import Dict
from typing import Dict, Optional, List
from datetime import *
from config import DEFAULT_AUTHOR
from enums import Attributes, Args
from PyTeX.base import Attributes, Args
from PyTeX.errors import *
from PyTeX.utils import ensure_file_integrity
from .formatter import Formatter
class PackageFormatter:
def __init__(self, package_name: str, author: str = DEFAULT_AUTHOR, extra_header: str = ""):
self.extra_header = extra_header
self.package_name_raw = package_name
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.package_name = r'{prefix}-{name}'.format(prefix=self.author_acronym,
name=self.package_name_raw.lower().strip().replace(' ', '-'))
self.package_prefix = self.package_name.replace('-', '@') + '@'
self.file_name = self.package_name + '.sty'
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):
def __command_name2keyword(keyword: str):
return '__' + keyword.upper().strip().replace(' ', '_') + '__'
def parse_replacement_args(self, match_groups, *user_args, **user_kwargs):
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:
@ -52,24 +76,12 @@ class PackageFormatter:
new_kwargs[kw] = 'ERROR'
return new_args, new_kwargs
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(self, contents: str) -> str:
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_with_arg(self, contents: str) -> str:
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,
@ -77,7 +89,7 @@ class PackageFormatter:
))
match = re.search(search_regex, contents)
while match is not None:
format_args, format_kwargs = self.parse_replacement_args(
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']
@ -89,14 +101,39 @@ class PackageFormatter:
match = re.search(search_regex, contents)
return contents
def format_package(self, input_path: Path, output_dir: Path = None):
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()
newlines = []
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_with_arg(self.format(line))
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)]

5
macros/__init__.py Normal file
View file

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

74
macros/default_macros.py Normal file
View file

@ -0,0 +1,74 @@
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())

View file

@ -1,62 +0,0 @@
from enums import Attributes, Args
from package_formatter import PackageFormatter
from config import LICENSE, PACKAGE_INFO_TEXT, PYTEX_INFO_TEXT
def make_default_commands(package_formatter: PackageFormatter):
header = '%' * 80 + '\n' \
+ '\n'.join(map(lambda line: '% ' + line,
LICENSE + [''] + PACKAGE_INFO_TEXT + [''] + PYTEX_INFO_TEXT
+ [''] + package_formatter.extra_header)
) \
+ '\n' + '%' * 80 + '\n\n' \
+ '\\NeedsTeXFormat{{LaTeX2e}}\n' \
'\\ProvidesPackage{{{package_name}}}[{date} - {description}]\n\n'
package_formatter.add_arg_replacement(
1, 'header',
header,
package_name=Attributes.package_name,
date=Attributes.date,
description=Args.one,
year=Attributes.year,
copyright_holders=Attributes.author,
source_file=Attributes.source_file_name
)
package_formatter.add_replacement('package name', '{}', Attributes.package_name)
package_formatter.add_replacement('package prefix', '{}', Attributes.package_prefix)
package_formatter.add_arg_replacement(1, 'package macro', r'\{}{}', Attributes.package_prefix, Args.one)
package_formatter.add_replacement('file name', '{name}', name=Attributes.file_name)
package_formatter.add_replacement('date', '{}', Attributes.date)
package_formatter.add_replacement('author', '{}', Attributes.author)
package_formatter.add_arg_replacement(2, 'new if', r'\newif\if{prefix}{condition}\{prefix}{condition}{value}',
prefix=Attributes.package_prefix, condition=Args.one, value=Args.two)
package_formatter.add_arg_replacement(2, 'set if', r'\{prefix}{condition}{value}',
prefix=Attributes.package_prefix, condition=Args.one, value=Args.two)
package_formatter.add_arg_replacement(1, 'if', r'\if{prefix}{condition}', prefix=Attributes.package_prefix,
condition=Args.one)
package_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=Attributes.package_prefix)
package_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=Attributes.package_prefix)
package_formatter.add_arg_replacement(1, 'info', r'\PackageInfo{{{name}}}{{{info}}}', name=Attributes.package_name,
info=Args.one)
package_formatter.add_arg_replacement(1, 'warning', r'\PackageWarning{{{name}}}{{{warning}}}',
name=Attributes.package_name, warning=Args.one)
package_formatter.add_arg_replacement(1, 'error', r'\PackageError{{{name}}}{{{error}}}}',
name=Attributes.package_name, error=Args.one)
package_formatter.add_replacement('end options x',
r"\DeclareOptionX*{{\PackageWarning{{{package_name}}}"
r"{{Unknown '\CurrentOption'}}}}" + '\n' + r'\ProcessOptionsX\relax' + '\n',
package_name=Attributes.package_name)
package_formatter.add_replacement('end options',
r"\DeclareOption*{{\PackageWarning{{{package_name}}}"
r"{{Unknown '\CurrentOption'}}}}" + '\n' + r'\ProcessOptions\relax' + '\n',
package_name=Attributes.package_name)

7
utils/__init__.py Normal file
View file

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

12
utils/checksum.py Normal file
View file

@ -0,0 +1,12 @@
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()

18
utils/file_integrity.py Normal file
View file

@ -0,0 +1,18 @@
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))