fab-build 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fab/__init__.py +4 -0
- fab/__main__.py +2 -0
- fab/caching/__init__.py +5 -0
- fab/caching/diskcache.py +73 -0
- fab/caching/memory.py +26 -0
- fab/caching/nullcache.py +16 -0
- fab/caching/test_diskcache.py +52 -0
- fab/lang/__init__.py +0 -0
- fab/lang/astree.py +109 -0
- fab/lang/buildins.py +140 -0
- fab/lang/data.py +88 -0
- fab/lang/evaluator.py +119 -0
- fab/lang/grammar.lark +39 -0
- fab/lang/parser.py +89 -0
- fab/lang/test_buildins.py +55 -0
- fab/lang/test_evaluator.py +155 -0
- fab/lang/test_parser.py +17 -0
- fab/lang/util.py +54 -0
- fab/main.py +127 -0
- fab/model/__init__.py +11 -0
- fab/model/cache.py +14 -0
- fab/model/executor.py +8 -0
- fab/model/oci.py +1 -0
- fab/model/operation.py +43 -0
- fab/operations/__init__.py +17 -0
- fab/operations/extract.py +31 -0
- fab/operations/gcc.py +170 -0
- fab/operations/http_archive.py +20 -0
- fab/operations/http_get.py +37 -0
- fab/operations/test_http_get.py +112 -0
- fab/util/__init__.py +0 -0
- fab/util/hash_objects.py +28 -0
- fab/util/hash_path.py +27 -0
- fab/util/string_slices.py +18 -0
- fab/util/temp_file_structure.py +25 -0
- fab/util/test_hash_path.py +120 -0
- fab/util/test_string_slices.py +11 -0
- fab_build-0.0.1.dist-info/METADATA +7 -0
- fab_build-0.0.1.dist-info/RECORD +42 -0
- fab_build-0.0.1.dist-info/WHEEL +5 -0
- fab_build-0.0.1.dist-info/entry_points.txt +2 -0
- fab_build-0.0.1.dist-info/top_level.txt +1 -0
fab/__init__.py
ADDED
fab/__main__.py
ADDED
fab/caching/__init__.py
ADDED
fab/caching/diskcache.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ..model import Cache
|
|
4
|
+
|
|
5
|
+
from ..util.string_slices import string_slices
|
|
6
|
+
from ..util.hash_path import hash_path
|
|
7
|
+
|
|
8
|
+
import blake3
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DiskCacheError(RuntimeError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DiskCache(Cache):
|
|
16
|
+
def __init__(self, root: Path) -> None:
|
|
17
|
+
|
|
18
|
+
self.__ops = root / "v1/ops"
|
|
19
|
+
self.__ops.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
self.__blobs = root / "v1/blobs"
|
|
22
|
+
self.__blobs.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def __get_op_path(self, op_key: bytes) -> Path:
|
|
25
|
+
key_parts = string_slices(op_key.hex(), 2, 2)
|
|
26
|
+
return self.__ops / key_parts[0] / key_parts[1] / op_key.hex()
|
|
27
|
+
|
|
28
|
+
def __get_blob_path(self, blob_key: bytes) -> Path:
|
|
29
|
+
key_parts = string_slices(blob_key.hex(), 2, 2)
|
|
30
|
+
return self.__blobs / key_parts[0] / key_parts[1] / blob_key.hex()
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def has(self, op_key: bytes) -> bool:
|
|
34
|
+
op_path = self.__get_op_path(op_key)
|
|
35
|
+
return op_path.exists()
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def get_path(self, op_key: bytes) -> Path:
|
|
39
|
+
op_path = self.__get_op_path(op_key)
|
|
40
|
+
if not op_path.is_dir():
|
|
41
|
+
raise DiskCacheError(f"{op_path} does not exist")
|
|
42
|
+
|
|
43
|
+
object_link = op_path / "object"
|
|
44
|
+
if not object_link.is_symlink():
|
|
45
|
+
raise DiskCacheError(f"Corrupt cache, {object_link} is not a symlink")
|
|
46
|
+
|
|
47
|
+
blob_path = object_link.resolve()
|
|
48
|
+
|
|
49
|
+
return blob_path
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
53
|
+
if not path.exists():
|
|
54
|
+
raise DiskCacheError(f"Cannot store non-existent path {path} in cache")
|
|
55
|
+
|
|
56
|
+
blob_key = hash_path(blake3.blake3(), path)
|
|
57
|
+
|
|
58
|
+
blob_path = self.__get_blob_path(blob_key)
|
|
59
|
+
blob_path.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
obj_path = blob_path / path.name
|
|
61
|
+
if not obj_path.exists():
|
|
62
|
+
path.rename(obj_path)
|
|
63
|
+
|
|
64
|
+
op_path = self.__get_op_path(op_key)
|
|
65
|
+
if op_path.exists():
|
|
66
|
+
raise DiskCacheError(
|
|
67
|
+
f"Operation with key '{op_key.hex()}' already exists in cache"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
op_path.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
(op_path / "object").symlink_to(obj_path.relative_to(op_path, walk_up=True))
|
|
72
|
+
|
|
73
|
+
return obj_path
|
fab/caching/memory.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ..model import Cache
|
|
6
|
+
|
|
7
|
+
class MemoryCache(Cache):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.__cached_paths: dict[bytes, Path] = {}
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
def has(self, op_key: bytes) -> bool:
|
|
13
|
+
return op_key in self.__cached_paths
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
def get_path(self, op_key: bytes) -> Path:
|
|
17
|
+
return self.__cached_paths[op_key]
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
21
|
+
self.__cached_paths[op_key] = path
|
|
22
|
+
return path
|
|
23
|
+
|
|
24
|
+
def print_cache(self) -> None:
|
|
25
|
+
for key, value in self.__cached_paths.items():
|
|
26
|
+
print(f"{key.hex()}: {value}")
|
fab/caching/nullcache.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import override
|
|
3
|
+
from ..model import Cache
|
|
4
|
+
|
|
5
|
+
class NullCache(Cache):
|
|
6
|
+
@override
|
|
7
|
+
def has(self, op_key: bytes) -> bool:
|
|
8
|
+
return False
|
|
9
|
+
|
|
10
|
+
@override
|
|
11
|
+
def get_path(self, op_key: bytes) -> Path:
|
|
12
|
+
raise KeyError()
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
16
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ..util.hash_path import hash_path
|
|
5
|
+
from ..util.temp_file_structure import TempFileStructure
|
|
6
|
+
from .diskcache import DiskCache
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def path_key(path: Path) -> str:
|
|
10
|
+
hashfunc = hashlib.sha256()
|
|
11
|
+
return hash_path(hashfunc, path).hex()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_store_has_get_path():
|
|
15
|
+
with TempFileStructure(
|
|
16
|
+
[
|
|
17
|
+
("objects/file1", "file1"),
|
|
18
|
+
("cache_dir/", None),
|
|
19
|
+
]
|
|
20
|
+
) as root:
|
|
21
|
+
cache = DiskCache(root / "cache_dir")
|
|
22
|
+
file = root / "objects/file1"
|
|
23
|
+
op_key = bytes.fromhex("abcdef1234567890")
|
|
24
|
+
original_key = path_key(file)
|
|
25
|
+
|
|
26
|
+
assert not cache.has(op_key)
|
|
27
|
+
cached_file = cache.store_path(op_key, file)
|
|
28
|
+
|
|
29
|
+
assert path_key(cached_file) == original_key
|
|
30
|
+
assert cache.has(op_key)
|
|
31
|
+
|
|
32
|
+
retrieved_file = cache.get_path(op_key)
|
|
33
|
+
assert path_key(retrieved_file) == original_key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_store_again():
|
|
37
|
+
with TempFileStructure(
|
|
38
|
+
[
|
|
39
|
+
("objects1/file", "file"),
|
|
40
|
+
("objects2/file", "file"),
|
|
41
|
+
("cache_dir/", None),
|
|
42
|
+
]
|
|
43
|
+
) as root:
|
|
44
|
+
cache = DiskCache(root / "cache_dir")
|
|
45
|
+
file1 = root / "objects1/file"
|
|
46
|
+
file2 = root / "objects2/file"
|
|
47
|
+
op1_key = bytes.fromhex("abcd01")
|
|
48
|
+
op2_key = bytes.fromhex("abcd02")
|
|
49
|
+
cached_file1 = cache.store_path(op1_key, file1)
|
|
50
|
+
cached_file2 = cache.store_path(op2_key, file2)
|
|
51
|
+
|
|
52
|
+
assert path_key(cached_file1) == path_key(cached_file2)
|
fab/lang/__init__.py
ADDED
|
File without changes
|
fab/lang/astree.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from math import exp
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AstNode(abc.ABC):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Name(AstNode):
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
def __init__(self, name: str) -> None:
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f"<Name {self.name}>"
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return self.name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Expression(AstNode, abc.ABC):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LiteralString(Expression):
|
|
30
|
+
value: str
|
|
31
|
+
|
|
32
|
+
def __init__(self, value: str) -> None:
|
|
33
|
+
self.value = value
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
return f'<LiteralString "{self.value}">'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Call(Expression):
|
|
41
|
+
target: Expression
|
|
42
|
+
pos_args: list[Expression]
|
|
43
|
+
key_args: dict[Name, Expression]
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
target: Expression,
|
|
48
|
+
pos_args: list[Expression],
|
|
49
|
+
key_args: dict[Name, Expression],
|
|
50
|
+
) -> None:
|
|
51
|
+
self.target = target
|
|
52
|
+
self.pos_args = pos_args
|
|
53
|
+
self.key_args = key_args
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return (
|
|
58
|
+
f"<Call target={self.target}, args={self.pos_args}, kwargs={self.key_args}>"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Variable(Expression):
|
|
63
|
+
name: Name
|
|
64
|
+
|
|
65
|
+
def __init__(self, name: Name) -> None:
|
|
66
|
+
self.name = name
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return f"<Variable {self.name}>"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class List(Expression):
|
|
74
|
+
items: list[Expression]
|
|
75
|
+
|
|
76
|
+
def __init__(self, items: list[Expression]) -> None:
|
|
77
|
+
self.items = items
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def __repr__(self):
|
|
81
|
+
return f"<List {','.join((str(x) for x in self.items))}>"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ListComprehension(Expression):
|
|
85
|
+
expression: Expression
|
|
86
|
+
target: Name
|
|
87
|
+
iterable: Expression
|
|
88
|
+
|
|
89
|
+
def __init__(self, expression: Expression, target: Name, iterable: Expression):
|
|
90
|
+
self.expression = expression
|
|
91
|
+
self.target = target
|
|
92
|
+
self.iterable = iterable
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"<ListComprehension ...>"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AttributeRef(Expression):
|
|
100
|
+
target: Expression
|
|
101
|
+
name: Name
|
|
102
|
+
|
|
103
|
+
def __init__(self, target: Expression, name: Name) -> None:
|
|
104
|
+
self.target = target
|
|
105
|
+
self.name = name
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return f"<AttributeRef {self.target}.{self.name}>"
|
fab/lang/buildins.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from copy import copy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
from fab.operations.gcc import GccCompile, GccLink, GccCollectCompileCommands
|
|
6
|
+
|
|
7
|
+
from ..operations import Extract, HttpArchive, HttpGet
|
|
8
|
+
from .data import EvaluationContext, Function, List, Object, PathObj, String
|
|
9
|
+
from .evaluator import EvaluationError, evaluate_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoadFunc(Function):
|
|
13
|
+
@override
|
|
14
|
+
async def call(
|
|
15
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
16
|
+
) -> Object:
|
|
17
|
+
assert isinstance(args[0], String)
|
|
18
|
+
filename = Path(args[0].value)
|
|
19
|
+
|
|
20
|
+
if filename.is_absolute():
|
|
21
|
+
raise EvaluationError("Cannot load absolute path")
|
|
22
|
+
|
|
23
|
+
new_context = copy(context)
|
|
24
|
+
new_context.apply_filename(filename)
|
|
25
|
+
|
|
26
|
+
return evaluate_context(new_context)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PathFunc(Function):
|
|
30
|
+
@override
|
|
31
|
+
async def call(
|
|
32
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
33
|
+
) -> Object:
|
|
34
|
+
assert isinstance(args[0], String)
|
|
35
|
+
path_str = args[0].value
|
|
36
|
+
working_dir = context.get_current_file().parent
|
|
37
|
+
path = working_dir / path_str
|
|
38
|
+
if not path.exists():
|
|
39
|
+
raise EvaluationError(f"No such path '{path_str}' ('{path}')")
|
|
40
|
+
|
|
41
|
+
return PathObj(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LinkFunc(Function):
|
|
45
|
+
@override
|
|
46
|
+
async def call(
|
|
47
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
48
|
+
) -> Object:
|
|
49
|
+
assert isinstance(args[0], PathObj)
|
|
50
|
+
target_path = args[0].path
|
|
51
|
+
|
|
52
|
+
destination = context.get_current_file().parent
|
|
53
|
+
link_path = destination / target_path.name
|
|
54
|
+
|
|
55
|
+
if link_path.exists():
|
|
56
|
+
if not link_path.is_symlink():
|
|
57
|
+
raise EvaluationError(
|
|
58
|
+
f"Link target already exists and is not a symlink '{link_path}'"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
link_path.unlink()
|
|
62
|
+
|
|
63
|
+
link_path.symlink_to(target_path)
|
|
64
|
+
|
|
65
|
+
return PathObj(link_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class HttpGetFunc(Function):
|
|
69
|
+
@override
|
|
70
|
+
async def call(
|
|
71
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
72
|
+
) -> Object:
|
|
73
|
+
assert isinstance(args[0], String)
|
|
74
|
+
url = args[0].value
|
|
75
|
+
op = HttpGet(url)
|
|
76
|
+
result = await context.execute_operation(op)
|
|
77
|
+
return PathObj(result)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ExtractFunc(Function):
|
|
81
|
+
@override
|
|
82
|
+
async def call(
|
|
83
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
84
|
+
) -> Object:
|
|
85
|
+
assert isinstance(args[0], PathObj)
|
|
86
|
+
archive = args[0].path
|
|
87
|
+
op = Extract(archive)
|
|
88
|
+
result = await context.execute_operation(op)
|
|
89
|
+
return PathObj(result)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class HttpArchiveFunc(Function):
|
|
93
|
+
@override
|
|
94
|
+
async def call(
|
|
95
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
96
|
+
) -> Object:
|
|
97
|
+
assert isinstance(args[0], String)
|
|
98
|
+
url = args[0].value
|
|
99
|
+
op = HttpArchive(url)
|
|
100
|
+
result = await context.execute_operation(op)
|
|
101
|
+
return PathObj(result)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GccCompileFunc(Function):
|
|
105
|
+
@override
|
|
106
|
+
async def call(
|
|
107
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
108
|
+
) -> Object:
|
|
109
|
+
assert isinstance(args[0], PathObj)
|
|
110
|
+
assert isinstance(kwargs["includes"], List)
|
|
111
|
+
source = args[0].path
|
|
112
|
+
includes = [p.path for p in kwargs["includes"].items]
|
|
113
|
+
op = GccCompile(source, includes)
|
|
114
|
+
result = await context.execute_operation(op)
|
|
115
|
+
return PathObj(result)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GccLinkFunc(Function):
|
|
119
|
+
@override
|
|
120
|
+
async def call(
|
|
121
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
122
|
+
) -> Object:
|
|
123
|
+
assert isinstance(args[0], String)
|
|
124
|
+
assert isinstance(args[1], List)
|
|
125
|
+
outputname = args[0].value
|
|
126
|
+
objects = [item.path for item in args[1].items]
|
|
127
|
+
|
|
128
|
+
op = GccLink(outputname, objects)
|
|
129
|
+
result = await context.execute_operation(op)
|
|
130
|
+
return PathObj(result)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class GccCollectCompileCommandsFunc(Function):
|
|
134
|
+
@override
|
|
135
|
+
async def call(
|
|
136
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
137
|
+
) -> Object:
|
|
138
|
+
op = GccCollectCompileCommands()
|
|
139
|
+
result = await context.execute_operation(op)
|
|
140
|
+
return PathObj(result)
|
fab/lang/data.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Any, override
|
|
7
|
+
|
|
8
|
+
from ..model import Operation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Object(ABC):
|
|
12
|
+
def has_attr(self, name: str) -> bool:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
def attrs(self) -> Sequence[str]:
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
async def get_attr(self, name: str) -> Object:
|
|
19
|
+
raise KeyError
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def __eq__(self, value: object, /) -> bool:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class String(Object):
|
|
27
|
+
value: str
|
|
28
|
+
|
|
29
|
+
def __init__(self, value: str) -> None:
|
|
30
|
+
self.value = value
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def __eq__(self, other: object, /) -> bool:
|
|
34
|
+
if not isinstance(other, String):
|
|
35
|
+
return False
|
|
36
|
+
return self.value == other.value
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
return f'<String "{self.value}">'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class List(Object):
|
|
44
|
+
items: Sequence[Object]
|
|
45
|
+
|
|
46
|
+
def __init__(self, items: Sequence[Object]) -> None:
|
|
47
|
+
self.items = items
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def __eq__(self, other: object, /) -> bool:
|
|
51
|
+
if not isinstance(other, List):
|
|
52
|
+
return False
|
|
53
|
+
return self.items == other.items
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PathObj(Object):
|
|
57
|
+
path: pathlib.Path
|
|
58
|
+
|
|
59
|
+
def __init__(self, path: pathlib.Path) -> None:
|
|
60
|
+
self.path = path
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
def __eq__(self, other: object, /) -> bool:
|
|
64
|
+
if not isinstance(other, PathObj):
|
|
65
|
+
return False
|
|
66
|
+
return self.path == other.path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EvaluationContext(ABC):
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def apply_filename(self, path: pathlib.Path) -> None: ...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_current_file(self) -> pathlib.Path: ...
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def execute_operation(self, operation: Operation) -> Any: ...
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def buildins(self) -> Mapping[str, Object]: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Function(Object, ABC):
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def call(
|
|
87
|
+
self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
|
|
88
|
+
) -> Object: ...
|
fab/lang/evaluator.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from ..lang import astree
|
|
5
|
+
from .data import EvaluationContext, Function, List, Object, String
|
|
6
|
+
from .parser import parse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EvaluationError(RuntimeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def evaluate_context(context: EvaluationContext) -> Object:
|
|
14
|
+
source = context.get_current_file().read_text()
|
|
15
|
+
|
|
16
|
+
assignments = parse(source)
|
|
17
|
+
|
|
18
|
+
class LoadedObject(Object):
|
|
19
|
+
@override
|
|
20
|
+
def has_attr(self, name: str) -> bool:
|
|
21
|
+
return name in assignments
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
async def get_attr(self, name: str) -> Object:
|
|
25
|
+
expr = assignments[name]
|
|
26
|
+
return await evaluate(expr, assignments, {}, context)
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
def attrs(self) -> Sequence[str]:
|
|
30
|
+
return list(assignments.keys())
|
|
31
|
+
|
|
32
|
+
return LoadedObject()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def evaluate(
|
|
36
|
+
expr: astree.Expression,
|
|
37
|
+
assignments: Mapping[str, astree.Expression],
|
|
38
|
+
locals: Mapping[str, Object],
|
|
39
|
+
context: EvaluationContext,
|
|
40
|
+
) -> Object:
|
|
41
|
+
|
|
42
|
+
async def evaluate_variable(expr: astree.Variable) -> Object:
|
|
43
|
+
var_name = expr.name.name
|
|
44
|
+
if var_name in locals:
|
|
45
|
+
return locals[var_name]
|
|
46
|
+
if var_name in context.buildins:
|
|
47
|
+
return context.buildins[var_name]
|
|
48
|
+
if var_name not in assignments:
|
|
49
|
+
raise EvaluationError(f"No variable named '{var_name}'")
|
|
50
|
+
return await evaluate(assignments[var_name], assignments, locals, context)
|
|
51
|
+
|
|
52
|
+
async def evaluate_literal_string(expr: astree.LiteralString) -> Object:
|
|
53
|
+
return String(expr.value)
|
|
54
|
+
|
|
55
|
+
async def evaluate_list(expr: astree.List) -> Object:
|
|
56
|
+
evaluated_items = [
|
|
57
|
+
await evaluate(item, assignments, locals, context) for item in expr.items
|
|
58
|
+
]
|
|
59
|
+
return List(evaluated_items)
|
|
60
|
+
|
|
61
|
+
async def evaluate_list_comprehension(expr: astree.ListComprehension) -> Object:
|
|
62
|
+
|
|
63
|
+
items = await evaluate(expr.iterable, assignments, locals, context)
|
|
64
|
+
assert isinstance(items, List)
|
|
65
|
+
|
|
66
|
+
evaluated_items = [
|
|
67
|
+
await evaluate(
|
|
68
|
+
expr.expression, assignments, {expr.target.name: item}, context
|
|
69
|
+
)
|
|
70
|
+
for item in items.items
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return List(evaluated_items)
|
|
74
|
+
|
|
75
|
+
async def evaluate_attribute_ref(expr: astree.AttributeRef) -> Object:
|
|
76
|
+
evaluated_target = await evaluate(expr.target, assignments, locals, context)
|
|
77
|
+
attr_name = expr.name.name
|
|
78
|
+
if not evaluated_target.has_attr(attr_name):
|
|
79
|
+
raise EvaluationError(f"No attribute named '{attr_name}'")
|
|
80
|
+
return await evaluated_target.get_attr(attr_name)
|
|
81
|
+
|
|
82
|
+
async def evaluate_call(expr: astree.Call) -> Object:
|
|
83
|
+
evaluated_target = await evaluate(expr.target, assignments, locals, context)
|
|
84
|
+
evaluated_args = [
|
|
85
|
+
await evaluate(arg, assignments, locals, context) for arg in expr.pos_args
|
|
86
|
+
]
|
|
87
|
+
evaluated_kwargs = {
|
|
88
|
+
kw.name: await evaluate(arg, assignments, locals, context)
|
|
89
|
+
for kw, arg in expr.key_args.items()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if isinstance(evaluated_target, Function):
|
|
93
|
+
return await evaluate_function_call(
|
|
94
|
+
evaluated_target, evaluated_args, evaluated_kwargs
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
raise EvaluationError("Target is not a callable")
|
|
98
|
+
|
|
99
|
+
async def evaluate_function_call(
|
|
100
|
+
target: Function,
|
|
101
|
+
args: list[Object],
|
|
102
|
+
kwargs: dict[str, Object],
|
|
103
|
+
) -> Object:
|
|
104
|
+
return await target.call(context, args, kwargs)
|
|
105
|
+
|
|
106
|
+
if isinstance(expr, astree.Variable):
|
|
107
|
+
return await evaluate_variable(expr)
|
|
108
|
+
elif isinstance(expr, astree.LiteralString):
|
|
109
|
+
return await evaluate_literal_string(expr)
|
|
110
|
+
elif isinstance(expr, astree.List):
|
|
111
|
+
return await evaluate_list(expr)
|
|
112
|
+
elif isinstance(expr, astree.ListComprehension):
|
|
113
|
+
return await evaluate_list_comprehension(expr)
|
|
114
|
+
elif isinstance(expr, astree.AttributeRef):
|
|
115
|
+
return await evaluate_attribute_ref(expr)
|
|
116
|
+
elif isinstance(expr, astree.Call):
|
|
117
|
+
return await evaluate_call(expr)
|
|
118
|
+
else:
|
|
119
|
+
raise NotImplementedError(f"Handling of {type(expr)} is not implemented")
|
fab/lang/grammar.lark
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
%import common.CNAME
|
|
2
|
+
%import common.WS
|
|
3
|
+
%import common.ESCAPED_STRING
|
|
4
|
+
|
|
5
|
+
COMMENT: /#[^\n]*/
|
|
6
|
+
|
|
7
|
+
%ignore WS
|
|
8
|
+
%ignore COMMENT
|
|
9
|
+
|
|
10
|
+
listing: assignment*
|
|
11
|
+
|
|
12
|
+
string: ESCAPED_STRING
|
|
13
|
+
name: CNAME
|
|
14
|
+
|
|
15
|
+
assignment: name "=" expression
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
?expression: string
|
|
19
|
+
| call
|
|
20
|
+
| variable
|
|
21
|
+
| list
|
|
22
|
+
| list_comprehension
|
|
23
|
+
| attributeref
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
variable: name
|
|
27
|
+
|
|
28
|
+
list: "[" [expression ("," expression)* [","] ] "]"
|
|
29
|
+
|
|
30
|
+
list_comprehension: "[" expression "for" name "in" expression "]"
|
|
31
|
+
|
|
32
|
+
attributeref: expression "." name
|
|
33
|
+
|
|
34
|
+
call: expression "(" [ call_args ] ")"
|
|
35
|
+
call_args: (arg ("," arg)* ["," kwarg ("," kwarg)* ] [","] )
|
|
36
|
+
| (kwarg ("," kwarg)* [","] )
|
|
37
|
+
?arg: expression
|
|
38
|
+
kwarg: name "=" expression
|
|
39
|
+
|