And fmt
This commit is contained in:
parent
debc7996ee
commit
bbae5ef63f
34 changed files with 956 additions and 886 deletions
|
@ -25,7 +25,6 @@ setup(
|
||||||
"calf-read = calf.reader:main",
|
"calf-read = calf.reader:main",
|
||||||
"calf-analyze = calf.analyzer:main",
|
"calf-analyze = calf.analyzer:main",
|
||||||
"calf-compile = calf.compiler:main",
|
"calf-compile = calf.compiler:main",
|
||||||
|
|
||||||
# Client/server stuff
|
# Client/server stuff
|
||||||
"calf-client = calf.client:main",
|
"calf-client = calf.client:main",
|
||||||
"calf-server = calf.server:main",
|
"calf-server = calf.server:main",
|
||||||
|
|
|
@ -7,7 +7,6 @@ from curses.textpad import Textbox, rectangle
|
||||||
|
|
||||||
|
|
||||||
def curse_repl(handle_buffer):
|
def curse_repl(handle_buffer):
|
||||||
|
|
||||||
def handle(buff, count):
|
def handle(buff, count):
|
||||||
try:
|
try:
|
||||||
return list(handle_buffer(buff, count)), None
|
return list(handle_buffer(buff, count)), None
|
||||||
|
@ -24,22 +23,25 @@ def curse_repl(handle_buffer):
|
||||||
maxy, maxx = stdscr.getmaxyx()
|
maxy, maxx = stdscr.getmaxyx()
|
||||||
stdscr.clear()
|
stdscr.clear()
|
||||||
|
|
||||||
stdscr.addstr(0, 0, "Enter example: (hit Ctrl-G to execute, Ctrl-C to exit)", curses.A_BOLD)
|
stdscr.addstr(
|
||||||
editwin = curses.newwin(5, maxx - 4,
|
0,
|
||||||
2, 2)
|
0,
|
||||||
rectangle(stdscr,
|
"Enter example: (hit Ctrl-G to execute, Ctrl-C to exit)",
|
||||||
1, 1,
|
curses.A_BOLD,
|
||||||
1 + 5 + 1, maxx - 2)
|
)
|
||||||
|
editwin = curses.newwin(5, maxx - 4, 2, 2)
|
||||||
|
rectangle(stdscr, 1, 1, 1 + 5 + 1, maxx - 2)
|
||||||
|
|
||||||
# Printing is part of the prompt
|
# Printing is part of the prompt
|
||||||
cur = 8
|
cur = 8
|
||||||
|
|
||||||
def putstr(str, x=0, attr=0):
|
def putstr(str, x=0, attr=0):
|
||||||
# ya rly. I know exactly what I'm doing here
|
# ya rly. I know exactly what I'm doing here
|
||||||
nonlocal cur
|
nonlocal cur
|
||||||
# This is how we handle going off the bottom of the scren lol
|
# This is how we handle going off the bottom of the scren lol
|
||||||
if cur < maxy:
|
if cur < maxy:
|
||||||
stdscr.addstr(cur, x, str, attr)
|
stdscr.addstr(cur, x, str, attr)
|
||||||
cur += (len(str.split("\n")) or 1)
|
cur += len(str.split("\n")) or 1
|
||||||
|
|
||||||
for ex, buff, vals, err in reversed(examples):
|
for ex, buff, vals, err in reversed(examples):
|
||||||
putstr(f"Example {ex}:", attr=curses.A_BOLD)
|
putstr(f"Example {ex}:", attr=curses.A_BOLD)
|
||||||
|
|
|
@ -28,34 +28,82 @@ COMMENT_PATTERN = r";(([^\n\r]*)(\n\r?)?)"
|
||||||
|
|
||||||
TOKENS = [
|
TOKENS = [
|
||||||
# Paren (noral) lists
|
# Paren (noral) lists
|
||||||
(r"\(", "PAREN_LEFT",),
|
(
|
||||||
(r"\)", "PAREN_RIGHT",),
|
r"\(",
|
||||||
|
"PAREN_LEFT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"\)",
|
||||||
|
"PAREN_RIGHT",
|
||||||
|
),
|
||||||
# Bracket lists
|
# Bracket lists
|
||||||
(r"\[", "BRACKET_LEFT",),
|
(
|
||||||
(r"\]", "BRACKET_RIGHT",),
|
r"\[",
|
||||||
|
"BRACKET_LEFT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"\]",
|
||||||
|
"BRACKET_RIGHT",
|
||||||
|
),
|
||||||
# Brace lists (maps)
|
# Brace lists (maps)
|
||||||
(r"\{", "BRACE_LEFT",),
|
(
|
||||||
(r"\}", "BRACE_RIGHT",),
|
r"\{",
|
||||||
(r"\^", "META",),
|
"BRACE_LEFT",
|
||||||
(r"'", "SINGLE_QUOTE",),
|
),
|
||||||
(STRING_PATTERN, "STRING",),
|
(
|
||||||
(r"#", "MACRO_DISPATCH",),
|
r"\}",
|
||||||
|
"BRACE_RIGHT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"\^",
|
||||||
|
"META",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"'",
|
||||||
|
"SINGLE_QUOTE",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
STRING_PATTERN,
|
||||||
|
"STRING",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"#",
|
||||||
|
"MACRO_DISPATCH",
|
||||||
|
),
|
||||||
# Symbols
|
# Symbols
|
||||||
(SYMBOL_PATTERN, "SYMBOL",),
|
(
|
||||||
|
SYMBOL_PATTERN,
|
||||||
|
"SYMBOL",
|
||||||
|
),
|
||||||
# Numbers
|
# Numbers
|
||||||
(SIMPLE_INTEGER, "INTEGER",),
|
(
|
||||||
(FLOAT_PATTERN, "FLOAT",),
|
SIMPLE_INTEGER,
|
||||||
|
"INTEGER",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FLOAT_PATTERN,
|
||||||
|
"FLOAT",
|
||||||
|
),
|
||||||
# Keywords
|
# Keywords
|
||||||
#
|
#
|
||||||
# Note: this is a dirty f'n hack in that in order for keywords to work, ":"
|
# Note: this is a dirty f'n hack in that in order for keywords to work, ":"
|
||||||
# has to be defined to be a valid keyword.
|
# has to be defined to be a valid keyword.
|
||||||
(r":" + SYMBOL_PATTERN + "?", "KEYWORD",),
|
(
|
||||||
|
r":" + SYMBOL_PATTERN + "?",
|
||||||
|
"KEYWORD",
|
||||||
|
),
|
||||||
# Whitespace
|
# Whitespace
|
||||||
#
|
#
|
||||||
# Note that the whitespace token will contain at most one newline
|
# Note that the whitespace token will contain at most one newline
|
||||||
(r"(\n\r?|[,\t ]*)", "WHITESPACE",),
|
(
|
||||||
|
r"(\n\r?|[,\t ]*)",
|
||||||
|
"WHITESPACE",
|
||||||
|
),
|
||||||
# Comment
|
# Comment
|
||||||
(COMMENT_PATTERN, "COMMENT",),
|
(
|
||||||
|
COMMENT_PATTERN,
|
||||||
|
"COMMENT",
|
||||||
|
),
|
||||||
# Strings
|
# Strings
|
||||||
(r'"(?P<body>(?:[^\"]|\.)*)"', "STRING"),
|
(r'"(?P<body>(?:[^\"]|\.)*)"', "STRING"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,7 +8,6 @@ parsing, linting or other use.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from calf.token import CalfToken
|
from calf.token import CalfToken
|
||||||
from calf.io.reader import PeekPosReader
|
from calf.io.reader import PeekPosReader
|
||||||
|
|
|
@ -12,8 +12,7 @@ from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
class CalfLoaderConfig(namedtuple("CalfLoaderConfig", ["paths"])):
|
class CalfLoaderConfig(namedtuple("CalfLoaderConfig", ["paths"])):
|
||||||
"""
|
""""""
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class CalfDelayedPackage(
|
class CalfDelayedPackage(
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
The Calf parser.
|
The Calf parser.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
from itertools import tee
|
from itertools import tee
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
from typing import NamedTuple, Callable
|
|
||||||
|
|
||||||
from calf.lexer import CalfLexer, lex_buffer, lex_file
|
from calf.lexer import CalfLexer, lex_buffer, lex_file
|
||||||
from calf.grammar import MATCHING, WHITESPACE_TYPES
|
from calf.grammar import MATCHING, WHITESPACE_TYPES
|
||||||
|
@ -45,17 +42,18 @@ def mk_dict(contents, open=None, close=None):
|
||||||
close.start_position,
|
close.start_position,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def mk_str(token):
|
def mk_str(token):
|
||||||
buff = token.value
|
buff = token.value
|
||||||
|
|
||||||
if buff.startswith('"""') and not buff.endswith('"""'):
|
if buff.startswith('"""') and not buff.endswith('"""'):
|
||||||
raise ValueError('Unterminated tripple quote string')
|
raise ValueError("Unterminated tripple quote string")
|
||||||
|
|
||||||
elif buff.startswith('"') and not buff.endswith('"'):
|
elif buff.startswith('"') and not buff.endswith('"'):
|
||||||
raise ValueError('Unterminated quote string')
|
raise ValueError("Unterminated quote string")
|
||||||
|
|
||||||
elif not buff.startswith('"') or buff == '"' or buff == '"""':
|
elif not buff.startswith('"') or buff == '"' or buff == '"""':
|
||||||
raise ValueError('Illegal string')
|
raise ValueError("Illegal string")
|
||||||
|
|
||||||
if buff.startswith('"""'):
|
if buff.startswith('"""'):
|
||||||
buff = buff[3:-3]
|
buff = buff[3:-3]
|
||||||
|
@ -114,15 +112,17 @@ class CalfMissingCloseParseError(CalfParseError):
|
||||||
def __init__(self, expected_close_token, open_token):
|
def __init__(self, expected_close_token, open_token):
|
||||||
super(CalfMissingCloseParseError, self).__init__(
|
super(CalfMissingCloseParseError, self).__init__(
|
||||||
f"expected {expected_close_token} starting from {open_token}, got end of file.",
|
f"expected {expected_close_token} starting from {open_token}, got end of file.",
|
||||||
open_token
|
open_token,
|
||||||
)
|
)
|
||||||
self.expected_close_token = expected_close_token
|
self.expected_close_token = expected_close_token
|
||||||
|
|
||||||
|
|
||||||
def parse_stream(stream,
|
def parse_stream(
|
||||||
|
stream,
|
||||||
discard_whitespace: bool = True,
|
discard_whitespace: bool = True,
|
||||||
discard_comments: bool = True,
|
discard_comments: bool = True,
|
||||||
stack: list = None):
|
stack: list = None,
|
||||||
|
):
|
||||||
"""Parses a token stream, producing a lazy sequence of all read top level forms.
|
"""Parses a token stream, producing a lazy sequence of all read top level forms.
|
||||||
|
|
||||||
If `discard_whitespace` is truthy, then no WHITESPACE tokens will be emitted
|
If `discard_whitespace` is truthy, then no WHITESPACE tokens will be emitted
|
||||||
|
@ -135,10 +135,9 @@ def parse_stream(stream,
|
||||||
stack = stack or []
|
stack = stack or []
|
||||||
|
|
||||||
def recur(_stack=None):
|
def recur(_stack=None):
|
||||||
yield from parse_stream(stream,
|
yield from parse_stream(
|
||||||
discard_whitespace,
|
stream, discard_whitespace, discard_comments, _stack or stack
|
||||||
discard_comments,
|
)
|
||||||
_stack or stack)
|
|
||||||
|
|
||||||
for token in stream:
|
for token in stream:
|
||||||
# Whitespace discarding
|
# Whitespace discarding
|
||||||
|
@ -205,7 +204,9 @@ def parse_stream(stream,
|
||||||
|
|
||||||
# Case of maybe matching something else, but definitely being wrong
|
# Case of maybe matching something else, but definitely being wrong
|
||||||
else:
|
else:
|
||||||
matching = next(reversed([t[1] for t in stack if t[0] == token.type]), None)
|
matching = next(
|
||||||
|
reversed([t[1] for t in stack if t[0] == token.type]), None
|
||||||
|
)
|
||||||
raise CalfUnexpectedCloseParseError(token, matching)
|
raise CalfUnexpectedCloseParseError(token, matching)
|
||||||
|
|
||||||
# Atoms
|
# Atoms
|
||||||
|
@ -216,18 +217,14 @@ def parse_stream(stream,
|
||||||
yield token
|
yield token
|
||||||
|
|
||||||
|
|
||||||
def parse_buffer(buffer,
|
def parse_buffer(buffer, discard_whitespace=True, discard_comments=True):
|
||||||
discard_whitespace=True,
|
|
||||||
discard_comments=True):
|
|
||||||
"""
|
"""
|
||||||
Parses a buffer, producing a lazy sequence of all parsed level forms.
|
Parses a buffer, producing a lazy sequence of all parsed level forms.
|
||||||
|
|
||||||
Propagates all errors.
|
Propagates all errors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yield from parse_stream(lex_buffer(buffer),
|
yield from parse_stream(lex_buffer(buffer), discard_whitespace, discard_comments)
|
||||||
discard_whitespace,
|
|
||||||
discard_comments)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_file(file):
|
def parse_file(file):
|
||||||
|
|
|
@ -13,6 +13,7 @@ from calf.parser import parse_stream
|
||||||
from calf.token import *
|
from calf.token import *
|
||||||
from calf.types import *
|
from calf.types import *
|
||||||
|
|
||||||
|
|
||||||
class CalfReader(object):
|
class CalfReader(object):
|
||||||
def handle_keyword(self, t: CalfToken) -> Any:
|
def handle_keyword(self, t: CalfToken) -> Any:
|
||||||
"""Convert a token to an Object value for a symbol.
|
"""Convert a token to an Object value for a symbol.
|
||||||
|
@ -79,8 +80,7 @@ class CalfReader(object):
|
||||||
return Vector.of(self.read(t.value))
|
return Vector.of(self.read(t.value))
|
||||||
|
|
||||||
elif isinstance(t, CalfDictToken):
|
elif isinstance(t, CalfDictToken):
|
||||||
return Map.of([(self.read1(k), self.read1(v))
|
return Map.of([(self.read1(k), self.read1(v)) for k, v in t.items()])
|
||||||
for k, v in t.items()])
|
|
||||||
|
|
||||||
# Magical pairwise stuff
|
# Magical pairwise stuff
|
||||||
elif isinstance(t, CalfQuoteToken):
|
elif isinstance(t, CalfQuoteToken):
|
||||||
|
@ -119,28 +119,21 @@ class CalfReader(object):
|
||||||
yield self.read1(t)
|
yield self.read1(t)
|
||||||
|
|
||||||
|
|
||||||
def read_stream(stream,
|
def read_stream(stream, reader: CalfReader = None):
|
||||||
reader: CalfReader = None):
|
"""Read from a stream of parsed tokens."""
|
||||||
"""Read from a stream of parsed tokens.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
reader = reader or CalfReader()
|
reader = reader or CalfReader()
|
||||||
yield from reader.read(stream)
|
yield from reader.read(stream)
|
||||||
|
|
||||||
|
|
||||||
def read_buffer(buffer):
|
def read_buffer(buffer):
|
||||||
"""Read from a buffer, producing a lazy sequence of all top level forms.
|
"""Read from a buffer, producing a lazy sequence of all top level forms."""
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
yield from read_stream(parse_stream(lex_buffer(buffer)))
|
yield from read_stream(parse_stream(lex_buffer(buffer)))
|
||||||
|
|
||||||
|
|
||||||
def read_file(file):
|
def read_file(file):
|
||||||
"""Read from a file, producing a lazy sequence of all top level forms.
|
"""Read from a file, producing a lazy sequence of all top level forms."""
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
yield from read_stream(parse_stream(lex_file(file)))
|
yield from read_stream(parse_stream(lex_file(file)))
|
||||||
|
|
||||||
|
@ -151,6 +144,8 @@ def main():
|
||||||
from calf.cursedrepl import curse_repl
|
from calf.cursedrepl import curse_repl
|
||||||
|
|
||||||
def handle_buffer(buff, count):
|
def handle_buffer(buff, count):
|
||||||
return list(read_stream(parse_stream(lex_buffer(buff, source=f"<Example {count}>"))))
|
return list(
|
||||||
|
read_stream(parse_stream(lex_buffer(buff, source=f"<Example {count}>")))
|
||||||
|
)
|
||||||
|
|
||||||
curse_repl(handle_buffer)
|
curse_repl(handle_buffer)
|
||||||
|
|
|
@ -8,23 +8,24 @@ from calf import grammar as cg
|
||||||
from conftest import parametrize
|
from conftest import parametrize
|
||||||
|
|
||||||
|
|
||||||
@parametrize('ex', [
|
@parametrize(
|
||||||
|
"ex",
|
||||||
|
[
|
||||||
# Proper strings
|
# Proper strings
|
||||||
'""',
|
'""',
|
||||||
'"foo bar"',
|
'"foo bar"',
|
||||||
'"foo\n bar\n\r qux"',
|
'"foo\n bar\n\r qux"',
|
||||||
'"foo\\"bar"',
|
'"foo\\"bar"',
|
||||||
|
|
||||||
'""""""',
|
'""""""',
|
||||||
'"""foo bar baz"""',
|
'"""foo bar baz"""',
|
||||||
'"""foo "" "" "" bar baz"""',
|
'"""foo "" "" "" bar baz"""',
|
||||||
|
|
||||||
# Unterminated string cases
|
# Unterminated string cases
|
||||||
'"',
|
'"',
|
||||||
'"f',
|
'"f',
|
||||||
'"foo bar',
|
'"foo bar',
|
||||||
'"foo\\" bar',
|
'"foo\\" bar',
|
||||||
'"""foo bar baz',
|
'"""foo bar baz',
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_match_string(ex):
|
def test_match_string(ex):
|
||||||
assert re.fullmatch(cg.STRING_PATTERN, ex)
|
assert re.fullmatch(cg.STRING_PATTERN, ex)
|
||||||
|
|
|
@ -20,23 +20,62 @@ def lex_single_token(buffer):
|
||||||
@parametrize(
|
@parametrize(
|
||||||
"text,token_type",
|
"text,token_type",
|
||||||
[
|
[
|
||||||
("(", "PAREN_LEFT",),
|
(
|
||||||
(")", "PAREN_RIGHT",),
|
"(",
|
||||||
("[", "BRACKET_LEFT",),
|
"PAREN_LEFT",
|
||||||
("]", "BRACKET_RIGHT",),
|
),
|
||||||
("{", "BRACE_LEFT",),
|
(
|
||||||
("}", "BRACE_RIGHT",),
|
")",
|
||||||
("^", "META",),
|
"PAREN_RIGHT",
|
||||||
("#", "MACRO_DISPATCH",),
|
),
|
||||||
|
(
|
||||||
|
"[",
|
||||||
|
"BRACKET_LEFT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"]",
|
||||||
|
"BRACKET_RIGHT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{",
|
||||||
|
"BRACE_LEFT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"}",
|
||||||
|
"BRACE_RIGHT",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"^",
|
||||||
|
"META",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"#",
|
||||||
|
"MACRO_DISPATCH",
|
||||||
|
),
|
||||||
("'", "SINGLE_QUOTE"),
|
("'", "SINGLE_QUOTE"),
|
||||||
("foo", "SYMBOL",),
|
(
|
||||||
|
"foo",
|
||||||
|
"SYMBOL",
|
||||||
|
),
|
||||||
("foo/bar", "SYMBOL"),
|
("foo/bar", "SYMBOL"),
|
||||||
(":foo", "KEYWORD",),
|
(
|
||||||
(":foo/bar", "KEYWORD",),
|
":foo",
|
||||||
(" ,,\t ,, \t", "WHITESPACE",),
|
"KEYWORD",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
":foo/bar",
|
||||||
|
"KEYWORD",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
" ,,\t ,, \t",
|
||||||
|
"WHITESPACE",
|
||||||
|
),
|
||||||
("\n\r", "WHITESPACE"),
|
("\n\r", "WHITESPACE"),
|
||||||
("\n", "WHITESPACE"),
|
("\n", "WHITESPACE"),
|
||||||
(" , ", "WHITESPACE",),
|
(
|
||||||
|
" , ",
|
||||||
|
"WHITESPACE",
|
||||||
|
),
|
||||||
("; this is a sample comment\n", "COMMENT"),
|
("; this is a sample comment\n", "COMMENT"),
|
||||||
('"foo"', "STRING"),
|
('"foo"', "STRING"),
|
||||||
('"foo bar baz"', "STRING"),
|
('"foo bar baz"', "STRING"),
|
||||||
|
|
|
@ -8,12 +8,15 @@ from conftest import parametrize
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@parametrize("text", [
|
@parametrize(
|
||||||
|
"text",
|
||||||
|
[
|
||||||
'"',
|
'"',
|
||||||
'"foo bar',
|
'"foo bar',
|
||||||
'"""foo bar',
|
'"""foo bar',
|
||||||
'"""foo bar"',
|
'"""foo bar"',
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_bad_strings_raise(text):
|
def test_bad_strings_raise(text):
|
||||||
"""Tests asserting we won't let obviously bad strings fly."""
|
"""Tests asserting we won't let obviously bad strings fly."""
|
||||||
# FIXME (arrdem 2021-03-13):
|
# FIXME (arrdem 2021-03-13):
|
||||||
|
@ -22,43 +25,55 @@ def test_bad_strings_raise(text):
|
||||||
next(cp.parse_buffer(text))
|
next(cp.parse_buffer(text))
|
||||||
|
|
||||||
|
|
||||||
@parametrize("text", [
|
@parametrize(
|
||||||
|
"text",
|
||||||
|
[
|
||||||
"[1.0",
|
"[1.0",
|
||||||
"(1.0",
|
"(1.0",
|
||||||
"{1.0",
|
"{1.0",
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_unterminated_raises(text):
|
def test_unterminated_raises(text):
|
||||||
"""Tests asserting that we don't let unterminated collections parse."""
|
"""Tests asserting that we don't let unterminated collections parse."""
|
||||||
with pytest.raises(cp.CalfMissingCloseParseError):
|
with pytest.raises(cp.CalfMissingCloseParseError):
|
||||||
next(cp.parse_buffer(text))
|
next(cp.parse_buffer(text))
|
||||||
|
|
||||||
|
|
||||||
@parametrize("text", [
|
@parametrize(
|
||||||
|
"text",
|
||||||
|
[
|
||||||
"[{]",
|
"[{]",
|
||||||
"[(]",
|
"[(]",
|
||||||
"({)",
|
"({)",
|
||||||
"([)",
|
"([)",
|
||||||
"{(}",
|
"{(}",
|
||||||
"{[}",
|
"{[}",
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_unbalanced_raises(text):
|
def test_unbalanced_raises(text):
|
||||||
"""Tests asserting that we don't let missmatched collections parse."""
|
"""Tests asserting that we don't let missmatched collections parse."""
|
||||||
with pytest.raises(cp.CalfUnexpectedCloseParseError):
|
with pytest.raises(cp.CalfUnexpectedCloseParseError):
|
||||||
next(cp.parse_buffer(text))
|
next(cp.parse_buffer(text))
|
||||||
|
|
||||||
|
|
||||||
@parametrize("buff, value", [
|
@parametrize(
|
||||||
|
"buff, value",
|
||||||
|
[
|
||||||
('"foo"', "foo"),
|
('"foo"', "foo"),
|
||||||
('"foo\tbar"', "foo\tbar"),
|
('"foo\tbar"', "foo\tbar"),
|
||||||
('"foo\n\rbar"', "foo\n\rbar"),
|
('"foo\n\rbar"', "foo\n\rbar"),
|
||||||
('"foo\\"bar\\""', "foo\"bar\""),
|
('"foo\\"bar\\""', 'foo"bar"'),
|
||||||
('"""foo"""', 'foo'),
|
('"""foo"""', "foo"),
|
||||||
('"""foo"bar"baz"""', 'foo"bar"baz'),
|
('"""foo"bar"baz"""', 'foo"bar"baz'),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_strings_round_trip(buff, value):
|
def test_strings_round_trip(buff, value):
|
||||||
assert next(cp.parse_buffer(buff)) == value
|
assert next(cp.parse_buffer(buff)) == value
|
||||||
|
|
||||||
@parametrize('text, element_types', [
|
|
||||||
|
@parametrize(
|
||||||
|
"text, element_types",
|
||||||
|
[
|
||||||
# Integers
|
# Integers
|
||||||
("(1)", ["INTEGER"]),
|
("(1)", ["INTEGER"]),
|
||||||
("( 1 )", ["INTEGER"]),
|
("( 1 )", ["INTEGER"]),
|
||||||
|
@ -66,13 +81,11 @@ def test_strings_round_trip(buff, value):
|
||||||
("(1\n)", ["INTEGER"]),
|
("(1\n)", ["INTEGER"]),
|
||||||
("(\n1\n)", ["INTEGER"]),
|
("(\n1\n)", ["INTEGER"]),
|
||||||
("(1, 2, 3, 4)", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
|
("(1, 2, 3, 4)", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
|
||||||
|
|
||||||
# Floats
|
# Floats
|
||||||
("(1.0)", ["FLOAT"]),
|
("(1.0)", ["FLOAT"]),
|
||||||
("(1.0e0)", ["FLOAT"]),
|
("(1.0e0)", ["FLOAT"]),
|
||||||
("(1e0)", ["FLOAT"]),
|
("(1e0)", ["FLOAT"]),
|
||||||
("(1e0)", ["FLOAT"]),
|
("(1e0)", ["FLOAT"]),
|
||||||
|
|
||||||
# Symbols
|
# Symbols
|
||||||
("(foo)", ["SYMBOL"]),
|
("(foo)", ["SYMBOL"]),
|
||||||
("(+)", ["SYMBOL"]),
|
("(+)", ["SYMBOL"]),
|
||||||
|
@ -82,7 +95,6 @@ def test_strings_round_trip(buff, value):
|
||||||
("(+foo-bar+)", ["SYMBOL"]),
|
("(+foo-bar+)", ["SYMBOL"]),
|
||||||
("(+foo-bar+)", ["SYMBOL"]),
|
("(+foo-bar+)", ["SYMBOL"]),
|
||||||
("( foo bar )", ["SYMBOL", "SYMBOL"]),
|
("( foo bar )", ["SYMBOL", "SYMBOL"]),
|
||||||
|
|
||||||
# Keywords
|
# Keywords
|
||||||
("(:foo)", ["KEYWORD"]),
|
("(:foo)", ["KEYWORD"]),
|
||||||
("( :foo )", ["KEYWORD"]),
|
("( :foo )", ["KEYWORD"]),
|
||||||
|
@ -90,13 +102,12 @@ def test_strings_round_trip(buff, value):
|
||||||
("(,:foo,)", ["KEYWORD"]),
|
("(,:foo,)", ["KEYWORD"]),
|
||||||
("(:foo :bar)", ["KEYWORD", "KEYWORD"]),
|
("(:foo :bar)", ["KEYWORD", "KEYWORD"]),
|
||||||
("(:foo :bar 1)", ["KEYWORD", "KEYWORD", "INTEGER"]),
|
("(:foo :bar 1)", ["KEYWORD", "KEYWORD", "INTEGER"]),
|
||||||
|
|
||||||
# Strings
|
# Strings
|
||||||
('("foo", "bar", "baz")', ["STRING", "STRING", "STRING"]),
|
('("foo", "bar", "baz")', ["STRING", "STRING", "STRING"]),
|
||||||
|
|
||||||
# Lists
|
# Lists
|
||||||
('([] [] ())', ["SQLIST", "SQLIST", "LIST"]),
|
("([] [] ())", ["SQLIST", "SQLIST", "LIST"]),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_parse_list(text, element_types):
|
def test_parse_list(text, element_types):
|
||||||
"""Test we can parse various lists of contents."""
|
"""Test we can parse various lists of contents."""
|
||||||
l_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
l_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
||||||
|
@ -104,7 +115,9 @@ def test_parse_list(text, element_types):
|
||||||
assert [t.type for t in l_t] == element_types
|
assert [t.type for t in l_t] == element_types
|
||||||
|
|
||||||
|
|
||||||
@parametrize('text, element_types', [
|
@parametrize(
|
||||||
|
"text, element_types",
|
||||||
|
[
|
||||||
# Integers
|
# Integers
|
||||||
("[1]", ["INTEGER"]),
|
("[1]", ["INTEGER"]),
|
||||||
("[ 1 ]", ["INTEGER"]),
|
("[ 1 ]", ["INTEGER"]),
|
||||||
|
@ -112,13 +125,11 @@ def test_parse_list(text, element_types):
|
||||||
("[1\n]", ["INTEGER"]),
|
("[1\n]", ["INTEGER"]),
|
||||||
("[\n1\n]", ["INTEGER"]),
|
("[\n1\n]", ["INTEGER"]),
|
||||||
("[1, 2, 3, 4]", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
|
("[1, 2, 3, 4]", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
|
||||||
|
|
||||||
# Floats
|
# Floats
|
||||||
("[1.0]", ["FLOAT"]),
|
("[1.0]", ["FLOAT"]),
|
||||||
("[1.0e0]", ["FLOAT"]),
|
("[1.0e0]", ["FLOAT"]),
|
||||||
("[1e0]", ["FLOAT"]),
|
("[1e0]", ["FLOAT"]),
|
||||||
("[1e0]", ["FLOAT"]),
|
("[1e0]", ["FLOAT"]),
|
||||||
|
|
||||||
# Symbols
|
# Symbols
|
||||||
("[foo]", ["SYMBOL"]),
|
("[foo]", ["SYMBOL"]),
|
||||||
("[+]", ["SYMBOL"]),
|
("[+]", ["SYMBOL"]),
|
||||||
|
@ -128,7 +139,6 @@ def test_parse_list(text, element_types):
|
||||||
("[+foo-bar+]", ["SYMBOL"]),
|
("[+foo-bar+]", ["SYMBOL"]),
|
||||||
("[+foo-bar+]", ["SYMBOL"]),
|
("[+foo-bar+]", ["SYMBOL"]),
|
||||||
("[ foo bar ]", ["SYMBOL", "SYMBOL"]),
|
("[ foo bar ]", ["SYMBOL", "SYMBOL"]),
|
||||||
|
|
||||||
# Keywords
|
# Keywords
|
||||||
("[:foo]", ["KEYWORD"]),
|
("[:foo]", ["KEYWORD"]),
|
||||||
("[ :foo ]", ["KEYWORD"]),
|
("[ :foo ]", ["KEYWORD"]),
|
||||||
|
@ -136,13 +146,12 @@ def test_parse_list(text, element_types):
|
||||||
("[,:foo,]", ["KEYWORD"]),
|
("[,:foo,]", ["KEYWORD"]),
|
||||||
("[:foo :bar]", ["KEYWORD", "KEYWORD"]),
|
("[:foo :bar]", ["KEYWORD", "KEYWORD"]),
|
||||||
("[:foo :bar 1]", ["KEYWORD", "KEYWORD", "INTEGER"]),
|
("[:foo :bar 1]", ["KEYWORD", "KEYWORD", "INTEGER"]),
|
||||||
|
|
||||||
# Strings
|
# Strings
|
||||||
('["foo", "bar", "baz"]', ["STRING", "STRING", "STRING"]),
|
('["foo", "bar", "baz"]', ["STRING", "STRING", "STRING"]),
|
||||||
|
|
||||||
# Lists
|
# Lists
|
||||||
('[[] [] ()]', ["SQLIST", "SQLIST", "LIST"]),
|
("[[] [] ()]", ["SQLIST", "SQLIST", "LIST"]),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_parse_sqlist(text, element_types):
|
def test_parse_sqlist(text, element_types):
|
||||||
"""Test we can parse various 'square' lists of contents."""
|
"""Test we can parse various 'square' lists of contents."""
|
||||||
l_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
l_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
||||||
|
@ -150,41 +159,21 @@ def test_parse_sqlist(text, element_types):
|
||||||
assert [t.type for t in l_t] == element_types
|
assert [t.type for t in l_t] == element_types
|
||||||
|
|
||||||
|
|
||||||
@parametrize('text, element_pairs', [
|
@parametrize(
|
||||||
("{}",
|
"text, element_pairs",
|
||||||
[]),
|
[
|
||||||
|
("{}", []),
|
||||||
("{:foo 1}",
|
("{:foo 1}", [["KEYWORD", "INTEGER"]]),
|
||||||
[["KEYWORD", "INTEGER"]]),
|
("{:foo 1, :bar 2}", [["KEYWORD", "INTEGER"], ["KEYWORD", "INTEGER"]]),
|
||||||
|
("{foo 1, bar 2}", [["SYMBOL", "INTEGER"], ["SYMBOL", "INTEGER"]]),
|
||||||
("{:foo 1, :bar 2}",
|
("{foo 1, bar -2}", [["SYMBOL", "INTEGER"], ["SYMBOL", "INTEGER"]]),
|
||||||
[["KEYWORD", "INTEGER"],
|
("{foo 1, bar -2e0}", [["SYMBOL", "INTEGER"], ["SYMBOL", "FLOAT"]]),
|
||||||
["KEYWORD", "INTEGER"]]),
|
("{foo ()}", [["SYMBOL", "LIST"]]),
|
||||||
|
("{foo []}", [["SYMBOL", "SQLIST"]]),
|
||||||
("{foo 1, bar 2}",
|
("{foo {}}", [["SYMBOL", "DICT"]]),
|
||||||
[["SYMBOL", "INTEGER"],
|
('{"foo" {}}', [["STRING", "DICT"]]),
|
||||||
["SYMBOL", "INTEGER"]]),
|
],
|
||||||
|
)
|
||||||
("{foo 1, bar -2}",
|
|
||||||
[["SYMBOL", "INTEGER"],
|
|
||||||
["SYMBOL", "INTEGER"]]),
|
|
||||||
|
|
||||||
("{foo 1, bar -2e0}",
|
|
||||||
[["SYMBOL", "INTEGER"],
|
|
||||||
["SYMBOL", "FLOAT"]]),
|
|
||||||
|
|
||||||
("{foo ()}",
|
|
||||||
[["SYMBOL", "LIST"]]),
|
|
||||||
|
|
||||||
("{foo []}",
|
|
||||||
[["SYMBOL", "SQLIST"]]),
|
|
||||||
|
|
||||||
("{foo {}}",
|
|
||||||
[["SYMBOL", "DICT"]]),
|
|
||||||
|
|
||||||
('{"foo" {}}',
|
|
||||||
[["STRING", "DICT"]])
|
|
||||||
])
|
|
||||||
def test_parse_dict(text, element_pairs):
|
def test_parse_dict(text, element_pairs):
|
||||||
"""Test we can parse various mappings."""
|
"""Test we can parse various mappings."""
|
||||||
d_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
d_t = next(cp.parse_buffer(text, discard_whitespace=True))
|
||||||
|
@ -192,19 +181,16 @@ def test_parse_dict(text, element_pairs):
|
||||||
assert [[t.type for t in pair] for pair in d_t.value] == element_pairs
|
assert [[t.type for t in pair] for pair in d_t.value] == element_pairs
|
||||||
|
|
||||||
|
|
||||||
@parametrize("text", [
|
@parametrize("text", ["{1}", "{1, 2, 3}", "{:foo}", "{:foo :bar :baz}"])
|
||||||
"{1}",
|
|
||||||
"{1, 2, 3}",
|
|
||||||
"{:foo}",
|
|
||||||
"{:foo :bar :baz}"
|
|
||||||
])
|
|
||||||
def test_parse_bad_dict(text):
|
def test_parse_bad_dict(text):
|
||||||
"""Assert that dicts with missmatched pairs don't parse."""
|
"""Assert that dicts with missmatched pairs don't parse."""
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
next(cp.parse_buffer(text))
|
next(cp.parse_buffer(text))
|
||||||
|
|
||||||
|
|
||||||
@parametrize("text", [
|
@parametrize(
|
||||||
|
"text",
|
||||||
|
[
|
||||||
"()",
|
"()",
|
||||||
"(1 1.1 1e2 -2 foo :foo foo/bar :foo/bar [{},])",
|
"(1 1.1 1e2 -2 foo :foo foo/bar :foo/bar [{},])",
|
||||||
"{:foo bar, :baz [:qux]}",
|
"{:foo bar, :baz [:qux]}",
|
||||||
|
@ -212,7 +198,8 @@ def test_parse_bad_dict(text):
|
||||||
"'[foo bar :baz 'qux, {}]",
|
"'[foo bar :baz 'qux, {}]",
|
||||||
"#foo []",
|
"#foo []",
|
||||||
"^{} bar",
|
"^{} bar",
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_examples(text):
|
def test_examples(text):
|
||||||
"""Shotgun examples showing we can parse some stuff."""
|
"""Shotgun examples showing we can parse some stuff."""
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@ from conftest import parametrize
|
||||||
|
|
||||||
from calf.reader import read_buffer
|
from calf.reader import read_buffer
|
||||||
|
|
||||||
@parametrize('text', [
|
|
||||||
|
@parametrize(
|
||||||
|
"text",
|
||||||
|
[
|
||||||
"()",
|
"()",
|
||||||
"[]",
|
"[]",
|
||||||
"[[[[[[[[[]]]]]]]]]",
|
"[[[[[[[[[]]]]]]]]]",
|
||||||
|
@ -17,6 +20,7 @@ from calf.reader import read_buffer
|
||||||
"^:foo bar",
|
"^:foo bar",
|
||||||
"{\"foo\" '([:bar ^:foo 'baz 3.14159e0])}",
|
"{\"foo\" '([:bar ^:foo 'baz 3.14159e0])}",
|
||||||
"[:foo bar 'baz lo/l, 1, 1.2. 1e-5 -1e2]",
|
"[:foo bar 'baz lo/l, 1, 1.2. 1e-5 -1e2]",
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_read(text):
|
def test_read(text):
|
||||||
assert list(read_buffer(text))
|
assert list(read_buffer(text))
|
||||||
|
|
|
@ -64,7 +64,7 @@ from datalog.types import (
|
||||||
LVar,
|
LVar,
|
||||||
PartlyIndexedDataset,
|
PartlyIndexedDataset,
|
||||||
Rule,
|
Rule,
|
||||||
TableIndexedDataset
|
TableIndexedDataset,
|
||||||
)
|
)
|
||||||
|
|
||||||
from prompt_toolkit import print_formatted_text, prompt, PromptSession
|
from prompt_toolkit import print_formatted_text, prompt, PromptSession
|
||||||
|
@ -74,12 +74,14 @@ from prompt_toolkit.styles import Style
|
||||||
from yaspin import Spinner, yaspin
|
from yaspin import Spinner, yaspin
|
||||||
|
|
||||||
|
|
||||||
STYLE = Style.from_dict({
|
STYLE = Style.from_dict(
|
||||||
|
{
|
||||||
# User input (default text).
|
# User input (default text).
|
||||||
"": "",
|
"": "",
|
||||||
"prompt": "ansigreen",
|
"prompt": "ansigreen",
|
||||||
"time": "ansiyellow"
|
"time": "ansiyellow",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SPINNER = Spinner(["|", "/", "-", "\\"], 200)
|
SPINNER = Spinner(["|", "/", "-", "\\"], 200)
|
||||||
|
|
||||||
|
@ -165,6 +167,7 @@ def main(args):
|
||||||
# .dbg drops to a debugger shell so you can poke at the instance objects (database)
|
# .dbg drops to a debugger shell so you can poke at the instance objects (database)
|
||||||
elif op == ".dbg":
|
elif op == ".dbg":
|
||||||
import pdb
|
import pdb
|
||||||
|
|
||||||
pdb.set_trace()
|
pdb.set_trace()
|
||||||
|
|
||||||
# .log sets the log level - badly
|
# .log sets the log level - badly
|
||||||
|
@ -180,10 +183,14 @@ def main(args):
|
||||||
# Syntax rules the parser doesn't impose...
|
# Syntax rules the parser doesn't impose...
|
||||||
try:
|
try:
|
||||||
for rule in val.rules():
|
for rule in val.rules():
|
||||||
assert not rule.free_vars, f"Rule contains free variables {rule.free_vars!r}"
|
assert (
|
||||||
|
not rule.free_vars
|
||||||
|
), f"Rule contains free variables {rule.free_vars!r}"
|
||||||
|
|
||||||
for tuple in val.tuples():
|
for tuple in val.tuples():
|
||||||
assert not any(isinstance(e, LVar) for e in tuple), f"Tuples cannot contain lvars - {tuple!r}"
|
assert not any(
|
||||||
|
isinstance(e, LVar) for e in tuple
|
||||||
|
), f"Tuples cannot contain lvars - {tuple!r}"
|
||||||
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
@ -233,8 +240,10 @@ def main(args):
|
||||||
# Retractions try to delete, but may fail.
|
# Retractions try to delete, but may fail.
|
||||||
elif op == "!":
|
elif op == "!":
|
||||||
if val in db.tuples() or val in [r.pattern for r in db.rules()]:
|
if val in db.tuples() or val in [r.pattern for r in db.rules()]:
|
||||||
db = db_cls([u for u in db.tuples() if u != val],
|
db = db_cls(
|
||||||
[r for r in db.rules() if r.pattern != val])
|
[u for u in db.tuples() if u != val],
|
||||||
|
[r for r in db.rules() if r.pattern != val],
|
||||||
|
)
|
||||||
print(f"⇒ {pr_str(val)}")
|
print(f"⇒ {pr_str(val)}")
|
||||||
else:
|
else:
|
||||||
print("⇒ Ø")
|
print("⇒ Ø")
|
||||||
|
@ -243,21 +252,26 @@ def main(args):
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
# Select which dataset type to use
|
# Select which dataset type to use
|
||||||
parser.add_argument("--db-type",
|
parser.add_argument(
|
||||||
|
"--db-type",
|
||||||
choices=["simple", "cached", "table", "partly"],
|
choices=["simple", "cached", "table", "partly"],
|
||||||
help="Choose which DB to use (default partly)",
|
help="Choose which DB to use (default partly)",
|
||||||
dest="db_cls",
|
dest="db_cls",
|
||||||
default="partly")
|
default="partly",
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument("--load-db", dest="dbs", action="append",
|
parser.add_argument(
|
||||||
help="Datalog files to load first.")
|
"--load-db", dest="dbs", action="append", help="Datalog files to load first."
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
args = parser.parse_args(sys.argv[1:])
|
args = parser.parse_args(sys.argv[1:])
|
||||||
logger = logging.getLogger("arrdem.datalog")
|
logger = logging.getLogger("arrdem.datalog")
|
||||||
ch = logging.StreamHandler()
|
ch = logging.StreamHandler()
|
||||||
ch.setLevel(logging.INFO)
|
ch.setLevel(logging.INFO)
|
||||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
formatter = logging.Formatter(
|
||||||
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
ch.setFormatter(formatter)
|
ch.setFormatter(formatter)
|
||||||
logger.addHandler(ch)
|
logger.addHandler(ch)
|
||||||
main(args)
|
main(args)
|
||||||
|
|
|
@ -23,10 +23,7 @@ setup(
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
],
|
],
|
||||||
|
scripts=["bin/datalog"],
|
||||||
scripts=[
|
|
||||||
"bin/datalog"
|
|
||||||
],
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"arrdem.datalog~=2.0.0",
|
"arrdem.datalog~=2.0.0",
|
||||||
"prompt_toolkit==2.0.9",
|
"prompt_toolkit==2.0.9",
|
||||||
|
|
|
@ -26,5 +26,7 @@ setup(
|
||||||
],
|
],
|
||||||
# Package setup
|
# Package setup
|
||||||
package_dir={"": "src/python"},
|
package_dir={"": "src/python"},
|
||||||
packages=["datalog",],
|
packages=[
|
||||||
|
"datalog",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,13 +27,19 @@ class Actions(object):
|
||||||
return self._db_cls(tuples, rules)
|
return self._db_cls(tuples, rules)
|
||||||
|
|
||||||
def make_symbol(self, input, start, end, elements):
|
def make_symbol(self, input, start, end, elements):
|
||||||
return LVar("".join(e.text for e in elements),)
|
return LVar(
|
||||||
|
"".join(e.text for e in elements),
|
||||||
|
)
|
||||||
|
|
||||||
def make_word(self, input, start, end, elements):
|
def make_word(self, input, start, end, elements):
|
||||||
return Constant("".join(e.text for e in elements),)
|
return Constant(
|
||||||
|
"".join(e.text for e in elements),
|
||||||
|
)
|
||||||
|
|
||||||
def make_string(self, input, start, end, elements):
|
def make_string(self, input, start, end, elements):
|
||||||
return Constant(elements[1].text,)
|
return Constant(
|
||||||
|
elements[1].text,
|
||||||
|
)
|
||||||
|
|
||||||
def make_comment(self, input, start, end, elements):
|
def make_comment(self, input, start, end, elements):
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -25,8 +25,19 @@ def test_id_query(db_cls):
|
||||||
Constant("a"),
|
Constant("a"),
|
||||||
Constant("b"),
|
Constant("b"),
|
||||||
)
|
)
|
||||||
assert not select(db_cls([], []), ("a", "b",))
|
assert not select(
|
||||||
assert select(db_cls([ab], []), ("a", "b",)) == [((("a", "b"),), {},)]
|
db_cls([], []),
|
||||||
|
(
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert select(db_cls([ab], []), ("a", "b",)) == [
|
||||||
|
(
|
||||||
|
(("a", "b"),),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("db_cls,", DBCLS)
|
@pytest.mark.parametrize("db_cls,", DBCLS)
|
||||||
|
@ -47,7 +58,17 @@ def test_lvar_unification(db_cls):
|
||||||
|
|
||||||
d = read("""edge(b, c). edge(c, c).""", db_cls=db_cls)
|
d = read("""edge(b, c). edge(c, c).""", db_cls=db_cls)
|
||||||
|
|
||||||
assert select(d, ("edge", "X", "X",)) == [((("edge", "c", "c"),), {"X": "c"})]
|
assert (
|
||||||
|
select(
|
||||||
|
d,
|
||||||
|
(
|
||||||
|
"edge",
|
||||||
|
"X",
|
||||||
|
"X",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
== [((("edge", "c", "c"),), {"X": "c"})]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("db_cls,", DBCLS)
|
@pytest.mark.parametrize("db_cls,", DBCLS)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name="arrdem.flowmetal",
|
name="arrdem.flowmetal",
|
||||||
# Package metadata
|
# Package metadata
|
||||||
version='0.0.0',
|
version="0.0.0",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
description="A weird execution engine",
|
description="A weird execution engine",
|
||||||
long_description=open("README.md").read(),
|
long_description=open("README.md").read(),
|
||||||
|
@ -18,20 +18,16 @@ setup(
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
],
|
],
|
||||||
|
|
||||||
# Package setup
|
# Package setup
|
||||||
package_dir={"": "src/python"},
|
package_dir={"": "src/python"},
|
||||||
packages=[
|
packages=[
|
||||||
"flowmetal",
|
"flowmetal",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": ["iflow=flowmetal.repl:main"],
|
||||||
'iflow=flowmetal.repl:main'
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'prompt-toolkit~=3.0.0',
|
"prompt-toolkit~=3.0.0",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
An abstract or base Flowmetal DB.
|
An abstract or base Flowmetal DB.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import abc, abstractmethod, abstractproperty, abstractclassmethod, abstractstaticmethod
|
from abc import abstractclassmethod, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class Db(ABC):
|
class Db(ABC):
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Somewhat generic models of Flowmetal programs.
|
Somewhat generic models of Flowmetal programs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -69,11 +69,15 @@ class GandiAPI(object):
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
def create_zone(self, zone_name):
|
def create_zone(self, zone_name):
|
||||||
new_zone_id = self._post("/zones",
|
new_zone_id = (
|
||||||
|
self._post(
|
||||||
|
"/zones",
|
||||||
headers={"content-type": "application/json"},
|
headers={"content-type": "application/json"},
|
||||||
data=json.dumps({"name": zone_name}))\
|
data=json.dumps({"name": zone_name}),
|
||||||
.headers["Location"]\
|
)
|
||||||
|
.headers["Location"]
|
||||||
.split("/")[-1]
|
.split("/")[-1]
|
||||||
|
)
|
||||||
new_zone = self.get_zone(new_zone_id)
|
new_zone = self.get_zone(new_zone_id)
|
||||||
|
|
||||||
# Note: If the cache is active, update the cache.
|
# Note: If the cache is active, update the cache.
|
||||||
|
@ -94,9 +98,13 @@ class GandiAPI(object):
|
||||||
print("Using zone", zone["uuid"])
|
print("Using zone", zone["uuid"])
|
||||||
|
|
||||||
for r in records:
|
for r in records:
|
||||||
self._post("/zones/{0}/records".format(zone["uuid"]),
|
self._post(
|
||||||
|
"/zones/{0}/records".format(zone["uuid"]),
|
||||||
headers={"content-type": "application/json"},
|
headers={"content-type": "application/json"},
|
||||||
data=json.dumps(r))
|
data=json.dumps(r),
|
||||||
|
)
|
||||||
|
|
||||||
return self._post("/zones/{0}/domains/{1}".format(zone["uuid"], domain),
|
return self._post(
|
||||||
headers={"content-type": "application/json"})
|
"/zones/{0}/domains/{1}".format(zone["uuid"], domain),
|
||||||
|
headers={"content-type": "application/json"},
|
||||||
|
)
|
||||||
|
|
|
@ -24,7 +24,8 @@ RECORD_LINE_PATTERN = re.compile(
|
||||||
"(?P<rrset_ttl>\S+)\s+"
|
"(?P<rrset_ttl>\S+)\s+"
|
||||||
"IN\s+"
|
"IN\s+"
|
||||||
"(?P<rrset_type>\S+)\s+"
|
"(?P<rrset_type>\S+)\s+"
|
||||||
"(?P<rrset_values>.+)$")
|
"(?P<rrset_values>.+)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update(m, k, f, *args, **kwargs):
|
def update(m, k, f, *args, **kwargs):
|
||||||
|
@ -47,8 +48,7 @@ def same_record(lr, rr):
|
||||||
A test to see if two records name the same zone entry.
|
A test to see if two records name the same zone entry.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return lr["rrset_name"] == rr["rrset_name"] and \
|
return lr["rrset_name"] == rr["rrset_name"] and lr["rrset_type"] == rr["rrset_type"]
|
||||||
lr["rrset_type"] == rr["rrset_type"]
|
|
||||||
|
|
||||||
|
|
||||||
def records_equate(lr, rr):
|
def records_equate(lr, rr):
|
||||||
|
@ -128,11 +128,14 @@ def diff_zones(left_zone, right_zone):
|
||||||
return in_left_not_right or in_right_not_left
|
return in_left_not_right or in_right_not_left
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="\"Dynamic\" DNS updating for self-hosted services")
|
parser = argparse.ArgumentParser(
|
||||||
|
description='"Dynamic" DNS updating for self-hosted services'
|
||||||
|
)
|
||||||
parser.add_argument("--config", dest="config_file", required=True)
|
parser.add_argument("--config", dest="config_file", required=True)
|
||||||
parser.add_argument("--templates", dest="template_dir", required=True)
|
parser.add_argument("--templates", dest="template_dir", required=True)
|
||||||
parser.add_argument("--dry-run", dest="dry", action="store_true", default=False)
|
parser.add_argument("--dry-run", dest="dry", action="store_true", default=False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
config = yaml.safe_load(open(args.config_file, "r"))
|
config = yaml.safe_load(open(args.config_file, "r"))
|
||||||
|
@ -142,27 +145,29 @@ def main():
|
||||||
device = config["meraki"]["router_serial"]
|
device = config["meraki"]["router_serial"]
|
||||||
|
|
||||||
uplinks = dashboard.appliance.getOrganizationApplianceUplinkStatuses(
|
uplinks = dashboard.appliance.getOrganizationApplianceUplinkStatuses(
|
||||||
organizationId=org,
|
organizationId=org, serials=[device]
|
||||||
serials=[device]
|
|
||||||
)[0]["uplinks"]
|
)[0]["uplinks"]
|
||||||
|
|
||||||
template_bindings = {
|
template_bindings = {
|
||||||
"local": {
|
"local": {
|
||||||
# One of the two
|
# One of the two
|
||||||
"public_v4s": [link.get("publicIp") for link in uplinks if link.get("publicIp")],
|
"public_v4s": [
|
||||||
|
link.get("publicIp") for link in uplinks if link.get("publicIp")
|
||||||
|
],
|
||||||
},
|
},
|
||||||
# Why isn't there a merge method
|
# Why isn't there a merge method
|
||||||
**config["bindings"]
|
**config["bindings"],
|
||||||
}
|
}
|
||||||
|
|
||||||
api = GandiAPI(config["gandi"]["key"])
|
api = GandiAPI(config["gandi"]["key"])
|
||||||
|
|
||||||
for task in config["tasks"]:
|
for task in config["tasks"]:
|
||||||
if isinstance(task, str):
|
if isinstance(task, str):
|
||||||
task = {"template": task + ".j2",
|
task = {"template": task + ".j2", "zones": [task]}
|
||||||
"zones": [task]}
|
|
||||||
|
|
||||||
computed_zone = template_and_parse_zone(os.path.join(args.template_dir, task["template"]), template_bindings)
|
computed_zone = template_and_parse_zone(
|
||||||
|
os.path.join(args.template_dir, task["template"]), template_bindings
|
||||||
|
)
|
||||||
|
|
||||||
for zone_name in task["zones"]:
|
for zone_name in task["zones"]:
|
||||||
try:
|
try:
|
||||||
|
@ -180,5 +185,6 @@ def main():
|
||||||
print("While processing zone {}".format(zone_name))
|
print("While processing zone {}".format(zone_name))
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__" or 1:
|
if __name__ == "__main__" or 1:
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -3,7 +3,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name="arrdem.ratchet",
|
name="arrdem.ratchet",
|
||||||
# Package metadata
|
# Package metadata
|
||||||
version='0.0.0',
|
version="0.0.0",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
description="A 'ratcheting' message system",
|
description="A 'ratcheting' message system",
|
||||||
long_description=open("README.md").read(),
|
long_description=open("README.md").read(),
|
||||||
|
@ -18,18 +18,12 @@ setup(
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
],
|
],
|
||||||
|
|
||||||
# Package setup
|
# Package setup
|
||||||
package_dir={
|
package_dir={"": "src/python"},
|
||||||
"": "src/python"
|
|
||||||
},
|
|
||||||
packages=[
|
packages=[
|
||||||
"ratchet",
|
"ratchet",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={},
|
||||||
},
|
install_requires=[],
|
||||||
install_requires=[
|
extras_require={},
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -98,14 +98,15 @@ VALUES (?, ?, ?, ?);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SQLiteDriver:
|
class SQLiteDriver:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
filename="~/.ratchet.sqlite3",
|
filename="~/.ratchet.sqlite3",
|
||||||
sqlite_timeout=1000,
|
sqlite_timeout=1000,
|
||||||
message_ttl=60000,
|
message_ttl=60000,
|
||||||
message_space="_",
|
message_space="_",
|
||||||
message_author=f"{os.getpid()}@{socket.gethostname()}"):
|
message_author=f"{os.getpid()}@{socket.gethostname()}",
|
||||||
|
):
|
||||||
self._path = os.path.expanduser(filename)
|
self._path = os.path.expanduser(filename)
|
||||||
self._sqlite_timeout = sqlite_timeout
|
self._sqlite_timeout = sqlite_timeout
|
||||||
self._message_ttl = message_ttl
|
self._message_ttl = message_ttl
|
||||||
|
@ -120,14 +121,11 @@ class SQLiteDriver:
|
||||||
conn.executescript(SCHEMA_SCRIPT)
|
conn.executescript(SCHEMA_SCRIPT)
|
||||||
|
|
||||||
def _connection(self):
|
def _connection(self):
|
||||||
return sql.connect(self._filename,
|
return sql.connect(self._filename, timeout=self._sqlite_timeout)
|
||||||
timeout=self._sqlite_timeout)
|
|
||||||
|
|
||||||
def create_message(self,
|
def create_message(
|
||||||
message: str,
|
self, message: str, ttl: int = None, space: str = None, author: str = None
|
||||||
ttl: int = None,
|
):
|
||||||
space: str = None,
|
|
||||||
author: str = None):
|
|
||||||
"""Create a single message."""
|
"""Create a single message."""
|
||||||
|
|
||||||
ttl = ttl or self._message_ttl
|
ttl = ttl or self._message_ttl
|
||||||
|
@ -138,11 +136,9 @@ class SQLiteDriver:
|
||||||
cursor.execute(CREATE_MESSAGE_SCRIPT, author, space, ttl, message)
|
cursor.execute(CREATE_MESSAGE_SCRIPT, author, space, ttl, message)
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
|
|
||||||
def create_event(self,
|
def create_event(
|
||||||
timeout: int,
|
self, timeout: int, ttl: int = None, space: str = None, author: str = None
|
||||||
ttl: int = None,
|
):
|
||||||
space: str = None,
|
|
||||||
author: str = None):
|
|
||||||
"""Create a (pending) event."""
|
"""Create a (pending) event."""
|
||||||
|
|
||||||
ttl = ttl or self._message_ttl
|
ttl = ttl or self._message_ttl
|
||||||
|
|
|
@ -7,111 +7,94 @@ from yamlschema import lint_buffer
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('schema, obj', [
|
@pytest.mark.parametrize(
|
||||||
({"type": "number"},
|
"schema, obj",
|
||||||
"---\n1.0"),
|
[
|
||||||
({"type": "integer"},
|
({"type": "number"}, "---\n1.0"),
|
||||||
"---\n3"),
|
({"type": "integer"}, "---\n3"),
|
||||||
({"type": "string"},
|
({"type": "string"}, "---\nfoo bar baz"),
|
||||||
"---\nfoo bar baz"),
|
({"type": "string", "maxLength": 15}, "---\nfoo bar baz"),
|
||||||
({"type": "string",
|
({"type": "string", "minLength": 10}, "---\nfoo bar baz"),
|
||||||
"maxLength": 15},
|
({"type": "string", "pattern": "^foo.*"}, "---\nfoo bar baz"),
|
||||||
"---\nfoo bar baz"),
|
({"type": "object", "additionalProperties": True}, "---\nfoo: bar\nbaz: qux"),
|
||||||
({"type": "string",
|
(
|
||||||
"minLength": 10},
|
{"type": "object", "properties": {"foo": {"type": "string"}}},
|
||||||
"---\nfoo bar baz"),
|
"---\nfoo: bar\nbaz: qux",
|
||||||
({"type": "string",
|
),
|
||||||
"pattern": "^foo.*"},
|
(
|
||||||
"---\nfoo bar baz"),
|
{
|
||||||
({"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": True},
|
|
||||||
"---\nfoo: bar\nbaz: qux"),
|
|
||||||
({"type": "object",
|
|
||||||
"properties": {"foo": {"type": "string"}}},
|
|
||||||
"---\nfoo: bar\nbaz: qux"),
|
|
||||||
({"type": "object",
|
|
||||||
"properties": {"foo": {"type": "string"}},
|
"properties": {"foo": {"type": "string"}},
|
||||||
"additionalProperties": False},
|
"additionalProperties": False,
|
||||||
"---\nfoo: bar"),
|
},
|
||||||
({"type": "object",
|
"---\nfoo: bar",
|
||||||
"properties": {"foo": {"type": "object"}}},
|
),
|
||||||
"---\nfoo: {}"),
|
({"type": "object", "properties": {"foo": {"type": "object"}}}, "---\nfoo: {}"),
|
||||||
({"type": "object",
|
(
|
||||||
"properties": {"foo": {
|
{
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {"type": "object"}}}},
|
"properties": {"foo": {"type": "array", "items": {"type": "object"}}},
|
||||||
"---\nfoo: [{}, {}, {foo: bar}]"),
|
},
|
||||||
])
|
"---\nfoo: [{}, {}, {foo: bar}]",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_lint_document_ok(schema, obj):
|
def test_lint_document_ok(schema, obj):
|
||||||
assert not list(lint_buffer(schema, obj))
|
assert not list(lint_buffer(schema, obj))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('msg, schema, obj', [
|
@pytest.mark.parametrize(
|
||||||
|
"msg, schema, obj",
|
||||||
|
[
|
||||||
# Numerics
|
# Numerics
|
||||||
("Floats are not ints",
|
("Floats are not ints", {"type": "integer"}, "---\n1.0"),
|
||||||
{"type": "integer"},
|
("Ints are not floats", {"type": "number"}, "---\n1"),
|
||||||
"---\n1.0"),
|
|
||||||
("Ints are not floats",
|
|
||||||
{"type": "number"},
|
|
||||||
"---\n1"),
|
|
||||||
|
|
||||||
# Numerics - range limits. Integer edition
|
# Numerics - range limits. Integer edition
|
||||||
("1 is the limit of the range",
|
(
|
||||||
{"type": "integer",
|
"1 is the limit of the range",
|
||||||
"exclusiveMaximum": 1},
|
{"type": "integer", "exclusiveMaximum": 1},
|
||||||
"---\n1"),
|
"---\n1",
|
||||||
("1 is the limit of the range",
|
),
|
||||||
{"type": "integer",
|
(
|
||||||
"exclusiveMinimum": 1},
|
"1 is the limit of the range",
|
||||||
"---\n1"),
|
{"type": "integer", "exclusiveMinimum": 1},
|
||||||
("1 is out of the range",
|
"---\n1",
|
||||||
{"type": "integer",
|
),
|
||||||
"minimum": 2},
|
("1 is out of the range", {"type": "integer", "minimum": 2}, "---\n1"),
|
||||||
"---\n1"),
|
("1 is out of the range", {"type": "integer", "maximum": 0}, "---\n1"),
|
||||||
("1 is out of the range",
|
("1 is out of the range", {"type": "integer", "exclusiveMinimum": 1}, "---\n1"),
|
||||||
{"type": "integer",
|
|
||||||
"maximum": 0},
|
|
||||||
"---\n1"),
|
|
||||||
("1 is out of the range",
|
|
||||||
{"type": "integer",
|
|
||||||
"exclusiveMinimum": 1},
|
|
||||||
"---\n1"),
|
|
||||||
|
|
||||||
# Numerics - range limits. Number/Float edition
|
# Numerics - range limits. Number/Float edition
|
||||||
("1 is the limit of the range",
|
(
|
||||||
{"type": "number",
|
"1 is the limit of the range",
|
||||||
"exclusiveMaximum": 1},
|
{"type": "number", "exclusiveMaximum": 1},
|
||||||
"---\n1.0"),
|
"---\n1.0",
|
||||||
("1 is the limit of the range",
|
),
|
||||||
{"type": "number",
|
(
|
||||||
"exclusiveMinimum": 1},
|
"1 is the limit of the range",
|
||||||
"---\n1.0"),
|
{"type": "number", "exclusiveMinimum": 1},
|
||||||
("1 is out of the range",
|
"---\n1.0",
|
||||||
{"type": "number",
|
),
|
||||||
"minimum": 2},
|
("1 is out of the range", {"type": "number", "minimum": 2}, "---\n1.0"),
|
||||||
"---\n1.0"),
|
("1 is out of the range", {"type": "number", "maximum": 0}, "---\n1.0"),
|
||||||
("1 is out of the range",
|
(
|
||||||
{"type": "number",
|
"1 is out of the range",
|
||||||
"maximum": 0},
|
{"type": "number", "exclusiveMinimum": 1},
|
||||||
"---\n1.0"),
|
"---\n1.0",
|
||||||
("1 is out of the range",
|
),
|
||||||
{"type": "number",
|
|
||||||
"exclusiveMinimum": 1},
|
|
||||||
"---\n1.0"),
|
|
||||||
|
|
||||||
# String shit
|
# String shit
|
||||||
("String too short",
|
("String too short", {"type": "string", "minLength": 1}, "---\n''"),
|
||||||
{"type": "string", "minLength": 1},
|
("String too long", {"type": "string", "maxLength": 1}, "---\nfoo"),
|
||||||
"---\n''"),
|
(
|
||||||
("String too long",
|
"String does not match pattern",
|
||||||
{"type": "string", "maxLength": 1},
|
|
||||||
"---\nfoo"),
|
|
||||||
("String does not match pattern",
|
|
||||||
{"type": "string", "pattern": "bar"},
|
{"type": "string", "pattern": "bar"},
|
||||||
"---\nfoo"),
|
"---\nfoo",
|
||||||
("String does not fully match pattern",
|
),
|
||||||
|
(
|
||||||
|
"String does not fully match pattern",
|
||||||
{"type": "string", "pattern": "foo"},
|
{"type": "string", "pattern": "foo"},
|
||||||
"---\nfooooooooo"),
|
"---\nfooooooooo",
|
||||||
])
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_lint_document_fails(msg, schema, obj):
|
def test_lint_document_fails(msg, schema, obj):
|
||||||
assert list(lint_buffer(schema, obj)), msg
|
assert list(lint_buffer(schema, obj)), msg
|
||||||
|
|
|
@ -59,9 +59,7 @@ class YamlLinter(object):
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def lint_mapping(self, schema, node: Node) -> t.Iterable[str]:
|
def lint_mapping(self, schema, node: Node) -> t.Iterable[str]:
|
||||||
"""FIXME.
|
"""FIXME."""
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if schema["type"] != "object" or not isinstance(node, MappingNode):
|
if schema["type"] != "object" or not isinstance(node, MappingNode):
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
|
@ -71,9 +69,7 @@ class YamlLinter(object):
|
||||||
f"Expected {schema['type']}, got {node.id} {str(node.start_mark).lstrip()}",
|
f"Expected {schema['type']}, got {node.id} {str(node.start_mark).lstrip()}",
|
||||||
)
|
)
|
||||||
|
|
||||||
additional_type: t.Union[dict, bool] = (
|
additional_type: t.Union[dict, bool] = schema.get("additionalProperties", True)
|
||||||
schema.get("additionalProperties", True)
|
|
||||||
)
|
|
||||||
properties: dict = schema.get("properties", {})
|
properties: dict = schema.get("properties", {})
|
||||||
required: t.Iterable[str] = schema.get("required", [])
|
required: t.Iterable[str] = schema.get("required", [])
|
||||||
|
|
||||||
|
@ -135,37 +131,26 @@ class YamlLinter(object):
|
||||||
elif schema["type"] == "number":
|
elif schema["type"] == "number":
|
||||||
yield from self.lint_number(schema, node)
|
yield from self.lint_number(schema, node)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(f"Scalar type {schema['type']} is not supported")
|
||||||
f"Scalar type {schema['type']} is not supported"
|
|
||||||
)
|
|
||||||
|
|
||||||
def lint_string(self, schema, node: Node) -> t.Iterable[str]:
|
def lint_string(self, schema, node: Node) -> t.Iterable[str]:
|
||||||
"""FIXME."""
|
"""FIXME."""
|
||||||
|
|
||||||
if node.tag != "tag:yaml.org,2002:str":
|
if node.tag != "tag:yaml.org,2002:str":
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH, node, schema, f"Expected a string, got a {node}"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
f"Expected a string, got a {node}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if maxl := schema.get("maxLength"):
|
if maxl := schema.get("maxLength"):
|
||||||
if len(node.value) > maxl:
|
if len(node.value) > maxl:
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH, node, schema, f"Expected a shorter string"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
f"Expected a shorter string"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if minl := schema.get("minLength"):
|
if minl := schema.get("minLength"):
|
||||||
if len(node.value) < minl:
|
if len(node.value) < minl:
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH, node, schema, f"Expected a longer string"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
f"Expected a longer string"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if pat := schema.get("pattern"):
|
if pat := schema.get("pattern"):
|
||||||
|
@ -174,7 +159,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a string matching the pattern"
|
f"Expected a string matching the pattern",
|
||||||
)
|
)
|
||||||
|
|
||||||
def lint_integer(self, schema, node: Node) -> t.Iterable[str]:
|
def lint_integer(self, schema, node: Node) -> t.Iterable[str]:
|
||||||
|
@ -184,10 +169,7 @@ class YamlLinter(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH, node, schema, f"Expected an integer, got a {node}"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
f"Expected an integer, got a {node}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def lint_number(self, schema, node: Node) -> t.Iterable[str]:
|
def lint_number(self, schema, node: Node) -> t.Iterable[str]:
|
||||||
|
@ -197,13 +179,9 @@ class YamlLinter(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH, node, schema, f"Expected an integer, got a {node}"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
f"Expected an integer, got a {node}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _lint_num_range(self, schema, node: Node, value) -> t.Iterable[str]:
|
def _lint_num_range(self, schema, node: Node, value) -> t.Iterable[str]:
|
||||||
""""FIXME."""
|
""""FIXME."""
|
||||||
|
|
||||||
|
@ -213,7 +191,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a multiple of {base}, got {value}"
|
f"Expected a multiple of {base}, got {value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (max := schema.get("exclusiveMaximum")) is not None:
|
if (max := schema.get("exclusiveMaximum")) is not None:
|
||||||
|
@ -222,7 +200,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a value less than {max}, got {value}"
|
f"Expected a value less than {max}, got {value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (max := schema.get("maximum")) is not None:
|
if (max := schema.get("maximum")) is not None:
|
||||||
|
@ -231,7 +209,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a value less than or equal to {max}, got {value}"
|
f"Expected a value less than or equal to {max}, got {value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (min := schema.get("exclusiveMinimum")) is not None:
|
if (min := schema.get("exclusiveMinimum")) is not None:
|
||||||
|
@ -240,7 +218,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a value greater than {min}, got {value}"
|
f"Expected a value greater than {min}, got {value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (min := schema.get("minimum")) is not None:
|
if (min := schema.get("minimum")) is not None:
|
||||||
|
@ -249,7 +227,7 @@ class YamlLinter(object):
|
||||||
LintLevel.MISSMATCH,
|
LintLevel.MISSMATCH,
|
||||||
node,
|
node,
|
||||||
schema,
|
schema,
|
||||||
f"Expected a value greater than or equal to {min}, got {value}"
|
f"Expected a value greater than or equal to {min}, got {value}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def lint_document(self, node, schema=None) -> t.Iterable[str]:
|
def lint_document(self, node, schema=None) -> t.Iterable[str]:
|
||||||
|
@ -271,10 +249,7 @@ class YamlLinter(object):
|
||||||
# This is the schema that rejects everything.
|
# This is the schema that rejects everything.
|
||||||
elif schema == False:
|
elif schema == False:
|
||||||
yield LintRecord(
|
yield LintRecord(
|
||||||
LintLevel.UNEXPECTED,
|
LintLevel.UNEXPECTED, node, schema, "Received an unexpected value"
|
||||||
node,
|
|
||||||
schema,
|
|
||||||
"Received an unexpected value"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Walking the PyYAML node hierarchy
|
# Walking the PyYAML node hierarchy
|
||||||
|
|
Loading…
Reference in a new issue