lispython 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {lispython-0.3.1 → lispython-0.3.2}/PKG-INFO +1 -1
- {lispython-0.3.1 → lispython-0.3.2}/pyproject.toml +1 -1
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/expr.py +8 -13
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/macro.py +10 -6
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/meta_functions.py +1 -1
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/parser.py +48 -3
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core_meta_functions.lpy +1 -1
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/sugar.lpy +2 -2
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/tools.lpy +15 -6
- lispython-0.3.2/tests/test_include_meta.py +37 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/test_literal.py +15 -0
- lispython-0.3.2/tests/test_meta_functions.py +26 -0
- {lispython-0.3.1 → lispython-0.3.2}/uv.lock +1 -1
- {lispython-0.3.1 → lispython-0.3.2}/.claude/settings.local.json +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/.gitignore +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/.pre-commit-config.yaml +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/LICENSE.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/README.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/index.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/macros.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/expressions.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/overview.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/statements.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/usage/cli.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/usage/getting-started.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/version_macro.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/docs/why-lispy.md +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/mkdocs.yml +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/__init__.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/__init__.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/literal.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/stmt.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/utils.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/importer.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/nodes.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/utils.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/__init__.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/init.lpy +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/__init__.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/test_expr.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/test_parser.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/test_stmt.py +0 -0
- {lispython-0.3.1 → lispython-0.3.2}/tests/utils.py +0 -0
|
@@ -359,20 +359,15 @@ def paren_compiler(sexp, ctx):
|
|
|
359
359
|
|
|
360
360
|
def slice_compile(sexp):
|
|
361
361
|
[_, *args] = sexp.list
|
|
362
|
-
|
|
362
|
+
assert 2 <= len(args) <= 3 # temporarily block length 1 slice for breaking change alert
|
|
363
|
+
[lower, upper, step] = args if len(args) == 3 else args + ["_"] if len(args) == 2 else ["_"] + args + ["_"]
|
|
363
364
|
args_dict = {}
|
|
364
|
-
if
|
|
365
|
-
lower =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if
|
|
369
|
-
|
|
370
|
-
if upper != "None" and upper != "_":
|
|
371
|
-
args_dict["upper"] = expr_compile(upper)
|
|
372
|
-
if args:
|
|
373
|
-
step = args.popleft()
|
|
374
|
-
if step != "None" and step != "_":
|
|
375
|
-
args_dict["step"] = expr_compile(step)
|
|
365
|
+
if lower != "None" and lower != "_":
|
|
366
|
+
args_dict["lower"] = expr_compile(lower)
|
|
367
|
+
if upper != "None" and upper != "_":
|
|
368
|
+
args_dict["upper"] = expr_compile(upper)
|
|
369
|
+
if step != "None" and step != "_":
|
|
370
|
+
args_dict["step"] = expr_compile(step)
|
|
376
371
|
return ast.Slice(**args_dict, **sexp.position_info)
|
|
377
372
|
|
|
378
373
|
|
|
@@ -13,7 +13,7 @@ def define_macro(sexp, scope, include_meta=True):
|
|
|
13
13
|
transformed = defmacro_transform(sexp)
|
|
14
14
|
eval(
|
|
15
15
|
compile(
|
|
16
|
-
ast.Interactive(body=macroexpand_then_compile([transformed])),
|
|
16
|
+
ast.Interactive(body=macroexpand_then_compile([transformed], include_meta=include_meta)),
|
|
17
17
|
"macro-defining",
|
|
18
18
|
"single",
|
|
19
19
|
),
|
|
@@ -26,7 +26,7 @@ def require_macro(sexp, scope, include_meta=True):
|
|
|
26
26
|
transformed = require_transform(sexp)
|
|
27
27
|
eval(
|
|
28
28
|
compile(
|
|
29
|
-
ast.Interactive(body=macroexpand_then_compile([transformed])),
|
|
29
|
+
ast.Interactive(body=macroexpand_then_compile([transformed], include_meta=include_meta)),
|
|
30
30
|
"macro-requiring",
|
|
31
31
|
"single",
|
|
32
32
|
),
|
|
@@ -49,14 +49,18 @@ def macroexpand_1(sexp, scope=globals()):
|
|
|
49
49
|
def macroexpand_1_and_check(sexp, scope=globals(), in_quasi=False, include_meta=True):
|
|
50
50
|
expanded = False
|
|
51
51
|
if isinstance(sexp, QuasiQuote):
|
|
52
|
-
sexp.value, expanded = macroexpand_1_and_check(
|
|
52
|
+
sexp.value, expanded = macroexpand_1_and_check(
|
|
53
|
+
sexp.value, scope, in_quasi=True, include_meta=include_meta
|
|
54
|
+
)
|
|
53
55
|
elif isinstance(sexp, Quote):
|
|
54
56
|
pass
|
|
55
57
|
elif isinstance(sexp, Unquote):
|
|
56
|
-
sexp.value, expanded = macroexpand_1_and_check(
|
|
58
|
+
sexp.value, expanded = macroexpand_1_and_check(
|
|
59
|
+
sexp.value, scope, include_meta=include_meta
|
|
60
|
+
)
|
|
57
61
|
elif isinstance(sexp, Wrapper):
|
|
58
62
|
sexp.value, expanded = macroexpand_1_and_check(
|
|
59
|
-
sexp.value, scope, in_quasi=in_quasi
|
|
63
|
+
sexp.value, scope, in_quasi=in_quasi, include_meta=include_meta
|
|
60
64
|
)
|
|
61
65
|
elif isinstance(sexp, Expression) and len(sexp) > 0:
|
|
62
66
|
[op, *operands] = sexp.list
|
|
@@ -70,7 +74,7 @@ def macroexpand_1_and_check(sexp, scope=globals(), in_quasi=False, include_meta=
|
|
|
70
74
|
else:
|
|
71
75
|
expanded_list, expanded_feedbacks = zip(
|
|
72
76
|
*map(
|
|
73
|
-
lambda x: macroexpand_1_and_check(x, scope, in_quasi=in_quasi),
|
|
77
|
+
lambda x: macroexpand_1_and_check(x, scope, in_quasi=in_quasi, include_meta=include_meta),
|
|
74
78
|
sexp.list,
|
|
75
79
|
)
|
|
76
80
|
)
|
|
@@ -122,6 +122,6 @@ if __name__ == "__main__":
|
|
|
122
122
|
path = osp.abspath(__file__)
|
|
123
123
|
with open(path, "r") as f:
|
|
124
124
|
org = f.read()
|
|
125
|
-
translated = l2py_s(org)
|
|
125
|
+
translated = l2py_s(org, no_lispy=True)
|
|
126
126
|
with open(osp.join(osp.dirname(path), "core", "meta_functions.py"), "w") as f:
|
|
127
127
|
f.write(translated)
|
|
@@ -26,9 +26,17 @@ def tokenize(src):
|
|
|
26
26
|
)
|
|
27
27
|
number_simple = "|".join(scientific_simples + float_simples + [int_simple])
|
|
28
28
|
complex_simple = f"(?:(?:{number_simple})[+-])?(?:{number_simple})j"
|
|
29
|
+
# F-string pattern with nested quotes in braces: f"...{expr "str"}..."
|
|
30
|
+
inner_str = r'\"(?:[^\"\\]|\\.)*\"'
|
|
31
|
+
brace_content = r'(?:[^{}\"\\]|\\.|\{[^{}]*\}|' + inner_str + r')*'
|
|
32
|
+
brace_expr = r'\{' + brace_content + r'\}'
|
|
33
|
+
f_string_content = r'(?:[^\"{}\\]|\\.|' + brace_expr + r')*'
|
|
34
|
+
f_string_pattern = r'f\"' + f_string_content + r'\"'
|
|
29
35
|
pattern_labels = [
|
|
30
36
|
["\\^?\\*{0,2}[\\(\\{\\[]", "opening"],
|
|
31
37
|
["[\\)\\}\\]]", "closing"],
|
|
38
|
+
# F-string pattern must come before regular string patterns
|
|
39
|
+
[f_string_pattern, '"'],
|
|
32
40
|
*list(
|
|
33
41
|
zip(
|
|
34
42
|
map(
|
|
@@ -260,15 +268,52 @@ def string_parse(token, tktype, position_info):
|
|
|
260
268
|
conversion_dict = {"!s": 115, "!r": 114, "!a": 97}
|
|
261
269
|
|
|
262
270
|
|
|
271
|
+
def find_format_spec_separator(piece):
|
|
272
|
+
"""Find the index of ':' that separates expression from format spec.
|
|
273
|
+
Only matches ':' at top level (not inside strings or brackets)."""
|
|
274
|
+
depth = 0
|
|
275
|
+
in_string = None
|
|
276
|
+
i = 0
|
|
277
|
+
while i < len(piece):
|
|
278
|
+
char = piece[i]
|
|
279
|
+
if in_string:
|
|
280
|
+
if char == '\\' and i + 1 < len(piece):
|
|
281
|
+
i += 2
|
|
282
|
+
continue
|
|
283
|
+
str_len = len(in_string)
|
|
284
|
+
if piece[i:i+str_len] == in_string:
|
|
285
|
+
in_string = None
|
|
286
|
+
i += str_len
|
|
287
|
+
continue
|
|
288
|
+
else:
|
|
289
|
+
if piece[i:i+3] in ('"""', "'''"):
|
|
290
|
+
in_string = piece[i:i+3]
|
|
291
|
+
i += 3
|
|
292
|
+
continue
|
|
293
|
+
elif char in ('"', "'"):
|
|
294
|
+
in_string = char
|
|
295
|
+
elif char in '([{':
|
|
296
|
+
depth += 1
|
|
297
|
+
elif char in ')]}':
|
|
298
|
+
depth -= 1
|
|
299
|
+
elif char == ':' and depth == 0:
|
|
300
|
+
return i
|
|
301
|
+
i += 1
|
|
302
|
+
return -1
|
|
303
|
+
|
|
304
|
+
|
|
263
305
|
def f_string_parse(prefix, content, tktype, position_info):
|
|
264
306
|
splitted = re.split("[\\{\\}]", content)
|
|
265
307
|
processed = []
|
|
266
308
|
splitted.pop() if splitted[-1] == "" else None
|
|
267
309
|
for [i, piece] in enumerate(splitted):
|
|
268
310
|
if i % 2:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
311
|
+
# Unescape \" to " (backslash-quote is escape for quotes inside f-string braces)
|
|
312
|
+
piece = piece.replace('\\"', '"')
|
|
313
|
+
sep_idx = find_format_spec_separator(piece)
|
|
314
|
+
if sep_idx >= 0:
|
|
315
|
+
format_spec = piece[sep_idx + 1:]
|
|
316
|
+
piece = piece[:sep_idx]
|
|
272
317
|
else:
|
|
273
318
|
format_spec = None
|
|
274
319
|
if piece[-2:] in conversion_dict:
|
|
@@ -89,8 +89,11 @@
|
|
|
89
89
|
(argparser.add-argument "--meta"
|
|
90
90
|
:dest "include_meta"
|
|
91
91
|
:action "store_true")
|
|
92
|
+
(argparser.add-argument "--no-lispy"
|
|
93
|
+
:dest "no_lispy"
|
|
94
|
+
:action "store_true")
|
|
92
95
|
|
|
93
|
-
(def l2py-s [src :include-meta False :fresh-scope False :package ""]
|
|
96
|
+
(def l2py-s [src :include-meta False :fresh-scope False :package "" :no-lispy False]
|
|
94
97
|
"Translate lispy source string to Python source string"
|
|
95
98
|
(if fresh-scope
|
|
96
99
|
(= scope {"__builtins__" __builtins__
|
|
@@ -98,9 +101,11 @@
|
|
|
98
101
|
"__macro_namespace" {}})
|
|
99
102
|
(= scope SCOPE))
|
|
100
103
|
(= pysrc (src-to-python src :include-meta include_meta :scope scope))
|
|
101
|
-
(
|
|
104
|
+
(if no_lispy
|
|
105
|
+
(return pysrc))
|
|
106
|
+
(return (+ "import lispy\n\n" pysrc)))
|
|
102
107
|
|
|
103
|
-
(def l2py-f [file-path :include-meta False]
|
|
108
|
+
(def l2py-f [file-path :include-meta False :no-lispy False]
|
|
104
109
|
"Translate lispy file to Python source string (standalone with sys.path setup)"
|
|
105
110
|
(= script-dir (osp.dirname (osp.abspath file-path)))
|
|
106
111
|
(if (not (in script-dir sys.path))
|
|
@@ -121,13 +126,13 @@
|
|
|
121
126
|
(with [(open file-path "rb") as f]
|
|
122
127
|
(= src (.decode (f.read) "utf-8")))
|
|
123
128
|
(= pkg (detect-package-name :path file-path))
|
|
124
|
-
(return (+ (l2py-s src :include-meta include_meta :fresh-scope True :package pkg) "\n")))
|
|
129
|
+
(return (+ (l2py-s src :include-meta include_meta :fresh-scope True :package pkg :no-lispy no_lispy) "\n")))
|
|
125
130
|
|
|
126
131
|
(def l2py []
|
|
127
132
|
"CLI entry point: parse args and translate file"
|
|
128
133
|
(= [args more-args] (argparser.parse-known-args))
|
|
129
134
|
(= file (osp.join (os.getcwd) (sub more-args 0)))
|
|
130
|
-
(print (l2py-f file :include-meta args.include_meta)))
|
|
135
|
+
(print (l2py-f file :include-meta args.include_meta :no-lispy args.no_lispy)))
|
|
131
136
|
|
|
132
137
|
(def read-multiline-input [prompt_str]
|
|
133
138
|
(= paren-depth 0)
|
|
@@ -173,9 +178,13 @@
|
|
|
173
178
|
)
|
|
174
179
|
|
|
175
180
|
(def repl [:translate False]
|
|
181
|
+
;; Add current directory to sys.path for local imports
|
|
182
|
+
(= cwd (os.getcwd))
|
|
183
|
+
(if (not (in cwd sys.path))
|
|
184
|
+
(sys.path.insert 0 cwd))
|
|
176
185
|
(= scope SCOPE)
|
|
177
186
|
(eval-and-print "(require lispy.macros *)
|
|
178
|
-
(from pprint [pprint])")
|
|
187
|
+
(from pprint [pprint])" scope)
|
|
179
188
|
(while True
|
|
180
189
|
(try
|
|
181
190
|
(do
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from lispy.tools import l2py_s
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_include_meta_flag_false():
|
|
5
|
+
src = "(defmacro m [x] x)"
|
|
6
|
+
out = l2py_s(src, include_meta=False)
|
|
7
|
+
# When include_meta is False, the defmacro form itself should not appear in the output
|
|
8
|
+
assert "defmacro" not in out and "def m" not in out
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_include_meta_flag_true():
|
|
12
|
+
src = "(defmacro m [x] x)"
|
|
13
|
+
out = l2py_s(src, include_meta=True)
|
|
14
|
+
# When include_meta is True, there should be some Python produced for the macro definition
|
|
15
|
+
assert "def" in out or "__macro_namespace" in out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_include_meta_nested():
|
|
19
|
+
src = "(list (defmacro m [x] x) 1)"
|
|
20
|
+
out_false = l2py_s(src, include_meta=False)
|
|
21
|
+
# Nested defmacro should not appear when include_meta is False
|
|
22
|
+
assert "defmacro" not in out_false and "def m" not in out_false and "__macro_namespace" not in out_false
|
|
23
|
+
|
|
24
|
+
out_true = l2py_s(src, include_meta=True)
|
|
25
|
+
# With include_meta True the macro definition/import should be present
|
|
26
|
+
assert "def" in out_true or "__macro_namespace" in out_true
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_import_lispy_default_and_no_lispy():
|
|
30
|
+
src = "(+ 1 2)"
|
|
31
|
+
out_default = l2py_s(src)
|
|
32
|
+
# By default we should prepend import lispy
|
|
33
|
+
assert out_default.startswith("import lispy")
|
|
34
|
+
|
|
35
|
+
out_no_lispy = l2py_s(src, no_lispy=True)
|
|
36
|
+
# With no_lispy True we should not add the import
|
|
37
|
+
assert not out_no_lispy.startswith("import lispy")
|
|
@@ -45,6 +45,21 @@ c"''') == "'a\\nb\\nc'"
|
|
|
45
45
|
ast.parse('f"sin({a}) is {sin(a):.3}"')
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
+
def test_f_string_with_nested_quotes(self):
|
|
49
|
+
# F-string with nested double-quoted string inside braces
|
|
50
|
+
# Uses raw string to have literal backslash-quote as in .lpy files
|
|
51
|
+
assert stmt_to_dump(
|
|
52
|
+
r'f"Started at {(.strftime (datetime.now) \"%Y-%m-%d %H:%M:%S\")}"'
|
|
53
|
+
) == ast.dump(
|
|
54
|
+
ast.parse('f"Started at {datetime.now().strftime(\'%Y-%m-%d %H:%M:%S\')}"')
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def test_f_string_with_colon_in_nested_string(self):
|
|
58
|
+
# Colon inside nested string should not be treated as format spec
|
|
59
|
+
assert stmt_to_dump(r'f"time: {(.get d \"H:M:S\")}"') == ast.dump(
|
|
60
|
+
ast.parse('f"time: {d.get(\'H:M:S\')}"')
|
|
61
|
+
)
|
|
62
|
+
|
|
48
63
|
|
|
49
64
|
class TestListMethods:
|
|
50
65
|
def test_list(self):
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os.path as osp
|
|
2
|
+
|
|
3
|
+
from lispy.tools import l2py_s
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestMetaFunctionsSync:
|
|
7
|
+
"""Test that core/meta_functions.py matches the transpiled result of core_meta_functions.lpy"""
|
|
8
|
+
|
|
9
|
+
def test_meta_functions_in_sync(self):
|
|
10
|
+
"""core/meta_functions.py should match transpiled core_meta_functions.lpy"""
|
|
11
|
+
base_dir = osp.dirname(osp.dirname(osp.abspath(__file__)))
|
|
12
|
+
lpy_path = osp.join(base_dir, "src", "lispy", "core_meta_functions.lpy")
|
|
13
|
+
py_path = osp.join(base_dir, "src", "lispy", "core", "meta_functions.py")
|
|
14
|
+
|
|
15
|
+
with open(lpy_path, "r") as f:
|
|
16
|
+
lpy_src = f.read()
|
|
17
|
+
|
|
18
|
+
with open(py_path, "r") as f:
|
|
19
|
+
py_contents = f.read()
|
|
20
|
+
|
|
21
|
+
transpiled = l2py_s(lpy_src)
|
|
22
|
+
|
|
23
|
+
assert transpiled == py_contents, (
|
|
24
|
+
"core/meta_functions.py is out of sync with core_meta_functions.lpy. "
|
|
25
|
+
"Run `lispy src/lispy/core_meta_functions.lpy` to regenerate."
|
|
26
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|