Skip to content

Commit

Permalink
Overhaul of context methods
Browse files Browse the repository at this point in the history
  • Loading branch information
sesquideus committed Dec 10, 2023
1 parent 41df0cb commit ff63aef
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 120 deletions.
82 changes: 53 additions & 29 deletions core/builder/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,35 @@

class Context(metaclass=ABCMeta):
defaults = {} # Defaults for every instance
_schema: Schema = None # Validation schema for the context, or None if it is not to be validated
_schema: Schema | None = None # Validation schema for the context, or None if it is not to be validated
_id: str = None
_data: dict = None

@property
def schema(self) -> Schema:
return self._schema

@property
def data(self) -> dict:
return self._data

@property
def id(self) -> str:
return self._id

@staticmethod
def _default(name, func=None, dfl=''):
if name is None:
return dfl
else:
return name if func is None else func(name)

def __init__(self, new_id=None, **defaults: dict):
self.id = new_id
self.data = copy.deepcopy(self.defaults)
def __init__(self, new_id=None, **defaults):
self._id = new_id
self._data = copy.deepcopy(self.defaults)

if defaults is not None:
self.add(defaults)
self.add(**defaults)

def __str__(self):
return f"<{self.__class__.__name__} named '{self.id}'>"
Expand All @@ -61,29 +71,22 @@ def validate(self) -> None:
pprint.pprint(self._schema.schema)
raise exc

def add(self, *dictionaries, overwrite=True):
""" Merge a list of dictionaries into this context, overwriting existing keys """
self.data = dicts.merge(self.data, *dictionaries, overwrite=overwrite)
def add(self, **kwargs):
""" Merge extra key-value pairs into this context, overwriting existing keys """
self._data |= kwargs
return self

def absorb(self, *contexts: Self):
""" Merge a list of other contexts into this context, overwriting existing keys """
for ctx in contexts:
self.data |= ctx.data
if self._schema is not None:
self._schema |= ctx.schema
return self

def adopt(self, key, ctx):
def adopt(self, **ctxs: 'Context') -> Self:
""" Adopt a new child context `ctx` under the key `key` """
assert isinstance(ctx, Context)
self.data[key] = dicts.merge(self.data.get(key), ctx.data)

if self._schema is not None:
# If child has no schema, accept anything, otherwise merge
self._schema |= Schema(
{key: {object: object} if ctx._schema is None else ctx._schema}
)
for key, ctx in ctxs.items():
assert isinstance(ctx, Context)
self.data[key] = dicts.merge(self.data.get(key), ctx.data)

if self._schema is not None:
# If child has no schema, accept anything, otherwise merge
self._schema |= Schema(
{key: {object: object} if ctx._schema is None else ctx._schema}
)
return self

def override(self, key, ctx):
Expand All @@ -94,10 +97,31 @@ def print(self):
pprint.pprint(self.data, width=120)

def add_number(self, number):
return self.add({'number': number})
return self.add(number=number)

def add_id(self, new_id):
return self.add({'id': new_id})
return self.add(id=new_id)

def __eq__(self, other):
return self.data == other.data and self.id == other.id

def __ior__(self, other):
if not isinstance(other, Context):
return NotImplemented
else:
self._data |= other.data

if self.schema is None or other.schema is None:
self._schema = None
else:
self._schema |= other.schema

return self

def __or__(self, other):
new = copy.deepcopy(self)
new |= other
return new


class FileSystemContext(Context, metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -142,15 +166,15 @@ def load_yaml(self, *args):
log.debug(f"Loading {c.name(self.__class__.__name__)} metadata from {c.path(filename)}")
try:
contents = yaml.load(open(filename, 'r'), Loader=yaml.SafeLoader)
contents = {} if contents is None else contents
self._data = {} if contents is None else contents
except FileNotFoundError as e:
log.critical(c.err("[FATAL] Could not load YAML file"), c.path(filename))
raise e

self.data = contents
return self

def load_meta(self, *path):
""" Shorthand for loading the node_path meta.yaml file """
return self.load_yaml(self.node_path(*path) / 'meta.yaml')

@abstractmethod
Expand Down
9 changes: 6 additions & 3 deletions core/builder/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
class FileSystemValidator(metaclass=abc.ABCMeta):
IGNORED = ['.git']

_schema: Schema | None = None
_schema: Schema = None

@property
def schema(self) -> Schema:
Expand All @@ -27,7 +27,7 @@ def __init__(self, root):
self.root = Path(root)
self.tree = self.scan(self.root)

def scan(self, path):
def scan(self, path) -> str | dict | None:
if path.name in self.IGNORED:
return None
if path.is_dir():
Expand All @@ -41,6 +41,9 @@ def scan(self, path):
return File

def validate(self) -> None:
"""
Validate the tree, re-raising the corresponding SchemaError if anything is out of order
"""
try:
self.schema.validate(self.tree)
self.perform_extra_checks()
Expand All @@ -53,6 +56,6 @@ def validate(self) -> None:
def perform_extra_checks(self) -> None:
"""
Extra checks that are impossible or cumbersome to implement with schema.
Runs *after* schema validation.
Runs *after* schema validation. By default, this is a no-op.
"""
pass
24 changes: 19 additions & 5 deletions core/tests/test_context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pytest
import math

from core.builder.context import Context

Expand All @@ -8,34 +7,40 @@
def context_empty():
return Context()


@pytest.fixture
def context_defaults():
return Context(foo='bar', baz=5)


@pytest.fixture
def context_two():
return Context(foo='hotel', qux=7)


@pytest.fixture
def context_old():
return Context(boss='Dušan', pictures='Plyš', htr='Kvík')


@pytest.fixture
def context_new():
return Context(boss='Marcel', pictures='Terka', nothing='Nina')


@pytest.fixture
def context_override(context_empty, context_old, context_new):
context_empty.adopt('fks', context_old)
context_empty.adopt('fks', context_new)
context_empty.adopt(fks=context_old)
context_empty.adopt(fks=context_new)
return context_empty


@pytest.fixture
def context_numbered():
return Context(id=123, number=456)


class TestContext():
class TestContext:
def test_empty(self, context_empty):
assert context_empty.data == {}

Expand Down Expand Up @@ -75,5 +80,14 @@ def test_add_number_override(self, context_defaults):
assert context_defaults.data['number'] == 666

def test_add(self, context_defaults, context_two):
context_defaults.absorb(context_two)
context_defaults |= context_two
assert context_defaults.data == dict(foo='hotel', baz=5, qux=7)

def test_ior(self):
first = Context('fks', boss='Matúš', coffee='Nina')
second = Context('htr', pictures='Terka', htr='Kvík', iy='Krto')
assert first | second == Context('fks', boss='Matúš', coffee='Nina', pictures='Terka', htr='Kvík', iy='Krto')

def test_or(self):
""" Note that or'ed contexts retain the parent's name but override items with child's """
assert Context('foo', bar='mitzvah') | Context('baz', bar='baron') == Context('foo', bar='baron')
19 changes: 11 additions & 8 deletions core/tests/test_convertor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def _convert(fmt, language, string):


class DisabledTestQuotes:
""" These tests are currently disabled: we have switched to `csquotes` """
def test_math_plus(self, convert):
assert convert('latex', 'sk', '"+"') == r'„+“' + '\n'

Expand All @@ -46,7 +47,8 @@ def test_english_interpunction(self, convert):
assert convert('latex', 'en', 'Ale "to" je "dobré", "nie."?') == r'Ale “to” je “dobré”, “nie.”?' + '\n'

def test_french_interpunction(self, convert):
assert convert('latex', 'fr', 'Ale "to" je "dobré", "nie."?') == r'Ale «\,to\,» je «\,dobré\,», «\,nie.\,»?' + '\n'
assert (convert('latex', 'fr', 'Ale "to" je "dobré", "nie."?') ==
r'Ale «\,to\,» je «\,dobré\,», «\,nie.\,»?' + '\n')

def test_spanish_interpunction(self, convert):
assert convert('latex', 'es', 'Ale "to" je "dobré", "nie."?') == r'Ale «to» je «dobré», «nie.»?' + '\n'
Expand All @@ -56,7 +58,7 @@ def test_english(self, convert):
assert output == r'Máme “dačo” a “niečo”. “Čo také?” \emph{“Asi nič.”}' + '\n'


class TestImages():
class TestImages:
def test_image_latex(self, convert):
output = convert('latex', 'sk', '![Masívna ryba](ryba.svg){#fig:ryba height=47mm}')
assert r'\insertPicture[width=\textwidth,height=47mm]{ryba.pdf}' in output
Expand All @@ -67,25 +69,26 @@ def test_image_latex_multiline(self, convert):
Veľmi masívne.
Aj s newlinami.](subor.png){#fig:dlhy height=53mm}
""")
assert re.search(r'\\insertPicture\[width=\\textwidth,height=53mm\]{subor\.png}', output) is not None
assert re.search(r'\\insertPicture\[width=\\textwidth,height=53mm]{subor\.png}', output) is not None

def test_image_html(self, convert):
output = convert('html', 'sk', '![Masívna ryba](ryba.svg){#fig:ryba height=47mm}')
assert re.match(r'<figure>.*<img.*src=".*ryba.svg".*<figcaption.*Masívna ryba.*</figcaption>.*</figure>', output, flags=re.DOTALL) is not None
assert re.match(r'<figure>.*<img.*src=".*ryba.svg".*<figcaption.*Masívna ryba.*</figcaption>.*</figure>',
output, flags=re.DOTALL) is not None

def test_image_html_multiline(self, convert):
output = convert('html', 'sk', """
![Veľmi dlhý text. Akože masívne.
Veľmi masívne.
Aj s newlinami.](subor.png){#fig:dlhy height=53mm}
""")
# Pandoc can split lines but we cannot tell how, so we replace newlines with spaces
# Pandoc can split lines, but we cannot tell how, so we replace newlines with spaces
output = output.replace('\n', ' ')
assert re.match(r'<figure>.*<img.* src=".*subor\.png".*<figcaption.*Veľmi dlhý text\. Akože masívne\. '
r'Veľmi masívne\. Aj s newlinami\..*</figcaption>.*</figure>', output) is not None


class TestTags():
class TestTags:
def test_h_latex(self, convert):
output = convert('latex', 'en', '@H this should not be seen!')
assert output == '\n'
Expand All @@ -104,12 +107,12 @@ def test_l_html(self, convert):

def test_e_latex(self, convert):
output = convert('latex', 'sk', '@E error')
assert re.match(r'\\errorMessage\{error\}\n', output) is not None
assert re.match(r'\\errorMessage\{error}\n', output) is not None

def test_e_html(self, convert):
output = convert('html', 'sk', '@E error')
assert re.match(r'<p>Error: error</p>', output) is not None

def test_aligned(self, convert):
output = convert('latex', 'sk', '$${\na\n}$$')
assert re.match(r'\\\[.*\\begin\{aligned\}\na\n\\end\{aligned\}.*\\\]', output, flags=re.DOTALL) is not None
assert re.match(r'\\\[.*\\begin\{aligned}\na\n\\end\{aligned}.*\\]', output, flags=re.DOTALL) is not None
8 changes: 4 additions & 4 deletions modules/naboj/builder/contexts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ def populate(self, repo_root: str):
- dgs branch and git hash
- repo branch and git hash
"""
self.add({
'build': {
self.add(
build={
'user': os.environ.get('USERNAME'),
'dgs': {
'hash': get_last_commit_hash(),
Expand All @@ -71,8 +71,8 @@ def populate(self, repo_root: str):
'branch': get_branch(self.node_path(repo_root)),
},
'timestamp': datetime.datetime.now(datetime.timezone.utc),
}
})
},
)

def as_tuple(self, competition: str = None, volume: int = None, sub: str = None, issue: str = None):
assert competition in ContextNaboj.competitions
Expand Down
25 changes: 14 additions & 11 deletions modules/naboj/builder/contexts/buildable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ class BuildableContextNaboj(BuildableFileSystemContext, ContextNaboj, metaclass=

def populate(self, competition, volume):
super().populate(competition)
self.adopt('module', ContextModule('naboj'))
self.adopt('competition', ContextCompetition(self.root, competition))
self.adopt('volume', ContextVolume(self.root, competition, volume))
self.adopt(
module=ContextModule('naboj'),
competition=ContextCompetition(self.root, competition),
volume=ContextVolume(self.root, competition, volume),
)


class BuildableContextLanguage(BuildableContextNaboj):
Expand All @@ -30,8 +32,10 @@ def __init__(self, root, *args):

def populate(self, competition, volume, language):
super().populate(competition, volume)
self.adopt('language', ContextLanguage(self.root, competition, volume, language))
self.adopt('i18n', ContextI18nGlobal(self.root, competition))
self.adopt(
language=ContextLanguage(self.root, competition, volume, language),
i18n=ContextI18nGlobal(self.root, competition),
)


class BuildableContextVenue(BuildableContextNaboj):
Expand All @@ -45,12 +49,11 @@ def __init__(self, root, *args):

def populate(self, competition, volume, venue):
super().populate(competition, volume)
self.adopt('venue', ContextVenue(self.root, competition, volume, venue)
.override('start', self.data['volume']['start']))
self.adopt('i18n', ContextI18nGlobal(self.root, competition))
self.add({
'language': i18n.languages[self.data['venue']['language']].as_dict()
})
self.adopt(
venue=ContextVenue(self.root, competition, volume, venue)
.override('start', self.data['volume']['start']),
i18n=ContextI18nGlobal(self.root, competition)
).add(language=i18n.languages[self.data['venue']['language']].as_dict())

if 'start' not in self.data['venue']:
self.data['venue']['start'] = self.data['volume']['start']
Loading

0 comments on commit ff63aef

Please sign in to comment.