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.
Files changed (43) hide show
  1. {lispython-0.3.1 → lispython-0.3.2}/PKG-INFO +1 -1
  2. {lispython-0.3.1 → lispython-0.3.2}/pyproject.toml +1 -1
  3. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/expr.py +8 -13
  4. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/macro.py +10 -6
  5. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/meta_functions.py +1 -1
  6. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/parser.py +48 -3
  7. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core_meta_functions.lpy +1 -1
  8. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/sugar.lpy +2 -2
  9. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/tools.lpy +15 -6
  10. lispython-0.3.2/tests/test_include_meta.py +37 -0
  11. {lispython-0.3.1 → lispython-0.3.2}/tests/test_literal.py +15 -0
  12. lispython-0.3.2/tests/test_meta_functions.py +26 -0
  13. {lispython-0.3.1 → lispython-0.3.2}/uv.lock +1 -1
  14. {lispython-0.3.1 → lispython-0.3.2}/.claude/settings.local.json +0 -0
  15. {lispython-0.3.1 → lispython-0.3.2}/.gitignore +0 -0
  16. {lispython-0.3.1 → lispython-0.3.2}/.pre-commit-config.yaml +0 -0
  17. {lispython-0.3.1 → lispython-0.3.2}/LICENSE.md +0 -0
  18. {lispython-0.3.1 → lispython-0.3.2}/README.md +0 -0
  19. {lispython-0.3.1 → lispython-0.3.2}/docs/index.md +0 -0
  20. {lispython-0.3.1 → lispython-0.3.2}/docs/macros.md +0 -0
  21. {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/expressions.md +0 -0
  22. {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/overview.md +0 -0
  23. {lispython-0.3.1 → lispython-0.3.2}/docs/syntax/statements.md +0 -0
  24. {lispython-0.3.1 → lispython-0.3.2}/docs/usage/cli.md +0 -0
  25. {lispython-0.3.1 → lispython-0.3.2}/docs/usage/getting-started.md +0 -0
  26. {lispython-0.3.1 → lispython-0.3.2}/docs/version_macro.py +0 -0
  27. {lispython-0.3.1 → lispython-0.3.2}/docs/why-lispy.md +0 -0
  28. {lispython-0.3.1 → lispython-0.3.2}/mkdocs.yml +0 -0
  29. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/__init__.py +0 -0
  30. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/__init__.py +0 -0
  31. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/literal.py +0 -0
  32. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/stmt.py +0 -0
  33. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/compiler/utils.py +0 -0
  34. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/importer.py +0 -0
  35. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/nodes.py +0 -0
  36. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/core/utils.py +0 -0
  37. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/__init__.py +0 -0
  38. {lispython-0.3.1 → lispython-0.3.2}/src/lispy/macros/init.lpy +0 -0
  39. {lispython-0.3.1 → lispython-0.3.2}/tests/__init__.py +0 -0
  40. {lispython-0.3.1 → lispython-0.3.2}/tests/test_expr.py +0 -0
  41. {lispython-0.3.1 → lispython-0.3.2}/tests/test_parser.py +0 -0
  42. {lispython-0.3.1 → lispython-0.3.2}/tests/test_stmt.py +0 -0
  43. {lispython-0.3.1 → lispython-0.3.2}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lispython
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Lisp-like Syntax for Python with Lisp-like Macros
5
5
  Project-URL: Homepage, https://jetack.github.io/lispython
6
6
  Project-URL: Repository, https://github.com/jetack/lispython
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lispython"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Lisp-like Syntax for Python with Lisp-like Macros"
5
5
  authors = [{ name = "Jetack", email = "jetack23@gmail.com" }]
6
6
  license = { text = "MIT" }
@@ -359,20 +359,15 @@ def paren_compiler(sexp, ctx):
359
359
 
360
360
  def slice_compile(sexp):
361
361
  [_, *args] = sexp.list
362
- args = deque(args)
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 args:
365
- lower = args.popleft()
366
- if lower != "None" and lower != "_":
367
- args_dict["lower"] = expr_compile(lower)
368
- if args:
369
- upper = args.popleft()
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(sexp.value, scope, in_quasi=True)
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(sexp.value, scope)
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
- if ":" in piece:
270
- [*quarks, format_spec] = piece.split(":")
271
- piece = ":".join(quarks)
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:
@@ -58,7 +58,7 @@
58
58
  (with [(open path "r") as f]
59
59
  (= org (f.read)))
60
60
 
61
- (= translated (l2py-s org))
61
+ (= translated (l2py-s org :no-lispy True))
62
62
 
63
63
  (with [(open (-> path
64
64
  (osp.dirname)
@@ -44,7 +44,7 @@
44
44
  (return `(- ~elt 1)))
45
45
 
46
46
  (defmacro ++ [elt]
47
- (return `(:= ~elt (+ ~elt 1))))
47
+ (return `(+= ~elt 1)))
48
48
 
49
49
  (defmacro -- [elt]
50
- (return `(:= ~elt (- ~elt 1))))
50
+ (return `(-= ~elt 1)))
@@ -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
- (return pysrc))
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
+ )
@@ -238,7 +238,7 @@ wheels = [
238
238
 
239
239
  [[package]]
240
240
  name = "lispython"
241
- version = "0.3.0"
241
+ version = "0.3.1"
242
242
  source = { editable = "." }
243
243
 
244
244
  [package.optional-dependencies]
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