lispython 0.3.0__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.0 → lispython-0.3.2}/PKG-INFO +6 -3
  2. {lispython-0.3.0 → lispython-0.3.2}/README.md +5 -2
  3. {lispython-0.3.0 → lispython-0.3.2}/pyproject.toml +1 -1
  4. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/compiler/expr.py +8 -13
  5. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/macro.py +10 -6
  6. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/meta_functions.py +15 -9
  7. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/parser.py +54 -5
  8. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core_meta_functions.lpy +8 -4
  9. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/macros/sugar.lpy +2 -2
  10. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/tools.lpy +24 -12
  11. lispython-0.3.2/tests/test_include_meta.py +37 -0
  12. {lispython-0.3.0 → lispython-0.3.2}/tests/test_literal.py +15 -0
  13. lispython-0.3.2/tests/test_meta_functions.py +26 -0
  14. {lispython-0.3.0 → lispython-0.3.2}/uv.lock +1 -1
  15. {lispython-0.3.0 → lispython-0.3.2}/.claude/settings.local.json +0 -0
  16. {lispython-0.3.0 → lispython-0.3.2}/.gitignore +0 -0
  17. {lispython-0.3.0 → lispython-0.3.2}/.pre-commit-config.yaml +0 -0
  18. {lispython-0.3.0 → lispython-0.3.2}/LICENSE.md +0 -0
  19. {lispython-0.3.0 → lispython-0.3.2}/docs/index.md +0 -0
  20. {lispython-0.3.0 → lispython-0.3.2}/docs/macros.md +0 -0
  21. {lispython-0.3.0 → lispython-0.3.2}/docs/syntax/expressions.md +0 -0
  22. {lispython-0.3.0 → lispython-0.3.2}/docs/syntax/overview.md +0 -0
  23. {lispython-0.3.0 → lispython-0.3.2}/docs/syntax/statements.md +0 -0
  24. {lispython-0.3.0 → lispython-0.3.2}/docs/usage/cli.md +0 -0
  25. {lispython-0.3.0 → lispython-0.3.2}/docs/usage/getting-started.md +0 -0
  26. {lispython-0.3.0 → lispython-0.3.2}/docs/version_macro.py +0 -0
  27. {lispython-0.3.0 → lispython-0.3.2}/docs/why-lispy.md +0 -0
  28. {lispython-0.3.0 → lispython-0.3.2}/mkdocs.yml +0 -0
  29. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/__init__.py +0 -0
  30. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/compiler/__init__.py +0 -0
  31. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/compiler/literal.py +0 -0
  32. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/compiler/stmt.py +0 -0
  33. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/compiler/utils.py +0 -0
  34. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/importer.py +0 -0
  35. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/nodes.py +0 -0
  36. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/core/utils.py +0 -0
  37. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/macros/__init__.py +0 -0
  38. {lispython-0.3.0 → lispython-0.3.2}/src/lispy/macros/init.lpy +0 -0
  39. {lispython-0.3.0 → lispython-0.3.2}/tests/__init__.py +0 -0
  40. {lispython-0.3.0 → lispython-0.3.2}/tests/test_expr.py +0 -0
  41. {lispython-0.3.0 → lispython-0.3.2}/tests/test_parser.py +0 -0
  42. {lispython-0.3.0 → lispython-0.3.2}/tests/test_stmt.py +0 -0
  43. {lispython-0.3.0 → 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.0
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
@@ -67,9 +67,12 @@ lpy -m unittest
67
67
  # Todo
68
68
  ## Environment
69
69
  - [ ] Test on more python versions
70
- - [ ] Some IDE plugins like hy-mode and jedhy for better editing experience.
70
+ - [ ] REPL should track history and arrow key navigation
71
+ - [ ] REPL multi-line input support
72
+ - [ ] Better compileation error messages
71
73
  ## Macro System
72
74
  - [ ] `as->` macro for syntactic sugar
73
75
  - [ ] `gensym` for avoiding name collision
74
76
  ## Python AST
75
- - [ ] `type_comment` never considered. Later, it should be covered
77
+ - [ ] `type_comment` never considered. Later, it should be covered
78
+ - [ ] Any missing AST nodes in the version 3.12+
@@ -46,9 +46,12 @@ lpy -m unittest
46
46
  # Todo
47
47
  ## Environment
48
48
  - [ ] Test on more python versions
49
- - [ ] Some IDE plugins like hy-mode and jedhy for better editing experience.
49
+ - [ ] REPL should track history and arrow key navigation
50
+ - [ ] REPL multi-line input support
51
+ - [ ] Better compileation error messages
50
52
  ## Macro System
51
53
  - [ ] `as->` macro for syntactic sugar
52
54
  - [ ] `gensym` for avoiding name collision
53
55
  ## Python AST
54
- - [ ] `type_comment` never considered. Later, it should be covered
56
+ - [ ] `type_comment` never considered. Later, it should be covered
57
+ - [ ] Any missing AST nodes in the version 3.12+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lispython"
3
- version = "0.3.0"
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
  )
@@ -34,6 +34,18 @@ def require_transform(sexp):
34
34
  [macro_names] = optional
35
35
  else:
36
36
  macro_names = None
37
+ module_name_str = str(module_name).replace("-", "_")
38
+ is_relative = module_name_str.startswith(".")
39
+ import_call = (
40
+ Paren(
41
+ Symbol("importlib.import-module"),
42
+ module_name_str,
43
+ Keyword(Symbol("package")),
44
+ Symbol("__package__"),
45
+ )
46
+ if is_relative
47
+ else Paren(Symbol("importlib.import-module"), module_name_str)
48
+ )
37
49
  return Paren(
38
50
  Symbol("do"),
39
51
  Paren(Symbol("import"), Symbol("importlib")),
@@ -41,13 +53,7 @@ def require_transform(sexp):
41
53
  Symbol("="),
42
54
  Symbol("___imported-macros"),
43
55
  Paren(
44
- Symbol("getattr"),
45
- Paren(
46
- Symbol("importlib.import-module"),
47
- str(module_name).replace("-", "_"),
48
- ),
49
- String('"__macro_namespace"'),
50
- Brace(),
56
+ Symbol("getattr"), import_call, String('"__macro_namespace"'), Brace()
51
57
  ),
52
58
  ),
53
59
  Paren(
@@ -111,11 +117,11 @@ def require_transform(sexp):
111
117
  if __name__ == "__main__":
112
118
  import os.path as osp
113
119
 
114
- from lispy.tools import l2py_f
120
+ from lispy.tools import l2py_s
115
121
 
116
122
  path = osp.abspath(__file__)
117
123
  with open(path, "r") as f:
118
124
  org = f.read()
119
- translated = l2py_f(org)
125
+ translated = l2py_s(org, no_lispy=True)
120
126
  with open(osp.join(osp.dirname(path), "core", "meta_functions.py"), "w") as f:
121
127
  f.write(translated)
@@ -7,8 +7,12 @@ from lispy.core.utils import augassignop_dict
7
7
 
8
8
 
9
9
  def tokenize(src):
10
- # Remove commas except those following { or ( with optional whitespace
11
- src = re.sub(r"([{(]\s*),|,", lambda m: m.group(0) if m.group(1) else " ", src)
10
+ # Remove commas except those following { or ( with optional whitespace, but preserve commas in strings
11
+ src = re.sub(
12
+ r'(\w*"""(?:[^\\"]|\\.)*"""|\w*\'\'\'(?:[^\\\']|\\.)*\'\'\'|\w*"(?:[^\\"]|\\.)*"|[{(]\s*,)|,',
13
+ lambda m: m.group(1) if m.group(1) else " ",
14
+ src,
15
+ )
12
16
  lines = src.split("\n")
13
17
  tokens = deque([])
14
18
  int_simple = r"\d+[\dA-Za-z]*(?:_[\dA-Za-z]+)*"
@@ -22,9 +26,17 @@ def tokenize(src):
22
26
  )
23
27
  number_simple = "|".join(scientific_simples + float_simples + [int_simple])
24
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'\"'
25
35
  pattern_labels = [
26
36
  ["\\^?\\*{0,2}[\\(\\{\\[]", "opening"],
27
37
  ["[\\)\\}\\]]", "closing"],
38
+ # F-string pattern must come before regular string patterns
39
+ [f_string_pattern, '"'],
28
40
  *list(
29
41
  zip(
30
42
  map(
@@ -256,15 +268,52 @@ def string_parse(token, tktype, position_info):
256
268
  conversion_dict = {"!s": 115, "!r": 114, "!a": 97}
257
269
 
258
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
+
259
305
  def f_string_parse(prefix, content, tktype, position_info):
260
306
  splitted = re.split("[\\{\\}]", content)
261
307
  processed = []
262
308
  splitted.pop() if splitted[-1] == "" else None
263
309
  for [i, piece] in enumerate(splitted):
264
310
  if i % 2:
265
- if ":" in piece:
266
- [*quarks, format_spec] = piece.split(":")
267
- 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]
268
317
  else:
269
318
  format_spec = None
270
319
  if piece[-2:] in conversion_dict:
@@ -21,11 +21,15 @@
21
21
  (if optional
22
22
  (= [macro-names] optional)
23
23
  (= macro-names None))
24
+ (= module-name-str (.replace (str module-name) "-" "_"))
25
+ (= is-relative (.startswith module-name-str "."))
26
+ (= import-call (ife is-relative
27
+ `(importlib.import-module ~module-name-str :package __package__)
28
+ `(importlib.import-module ~module-name-str)))
24
29
  (return
25
30
  `(do (import importlib)
26
31
  (= ___imported-macros
27
- (getattr (importlib.import-module
28
- ~(.replace (str module-name) "-" "_"))
32
+ (getattr ~import-call
29
33
  "__macro_namespace"
30
34
  {}))
31
35
  ~(conde (not macro-names)
@@ -47,14 +51,14 @@
47
51
 
48
52
  (when (== __name__ "__main__")
49
53
  (import os.path as osp)
50
- (from lispy.tools [l2py-f])
54
+ (from lispy.tools [l2py-s])
51
55
 
52
56
  (= path (osp.abspath __file__))
53
57
 
54
58
  (with [(open path "r") as f]
55
59
  (= org (f.read)))
56
60
 
57
- (= translated (l2py-f org))
61
+ (= translated (l2py-s org :no-lispy True))
58
62
 
59
63
  (with [(open (-> path
60
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)))
@@ -20,14 +20,16 @@
20
20
  (from lispy.core.parser [parse])
21
21
  (from lispy.core.macro [macroexpand-then-compile])
22
22
 
23
- (def detect-package-name []
24
- "Detect package name from current directory by checking for __init__.py"
25
- (= cwd (os.getcwd))
26
- (if (not (osp.exists (osp.join cwd "__init__.py")))
23
+ (def detect-package-name [:path None]
24
+ "Detect package name from directory by checking for __init__.py"
25
+ (= dir-path (ife (is path None)
26
+ (os.getcwd)
27
+ (osp.dirname (osp.abspath path))))
28
+ (if (not (osp.exists (osp.join dir-path "__init__.py")))
27
29
  (return ""))
28
30
  ;; Walk up to find package root
29
31
  (= parts [])
30
- (= current cwd)
32
+ (= current dir-path)
31
33
  (while (osp.exists (osp.join current "__init__.py"))
32
34
  (parts.insert 0 (osp.basename current))
33
35
  (= parent (osp.dirname current))
@@ -87,18 +89,23 @@
87
89
  (argparser.add-argument "--meta"
88
90
  :dest "include_meta"
89
91
  :action "store_true")
92
+ (argparser.add-argument "--no-lispy"
93
+ :dest "no_lispy"
94
+ :action "store_true")
90
95
 
91
- (def l2py-s [src :include-meta False :fresh-scope False]
96
+ (def l2py-s [src :include-meta False :fresh-scope False :package "" :no-lispy False]
92
97
  "Translate lispy source string to Python source string"
93
98
  (if fresh-scope
94
99
  (= scope {"__builtins__" __builtins__
95
- "__package__" ""
100
+ "__package__" package
96
101
  "__macro_namespace" {}})
97
102
  (= scope SCOPE))
98
103
  (= pysrc (src-to-python src :include-meta include_meta :scope scope))
99
- (return pysrc))
104
+ (if no_lispy
105
+ (return pysrc))
106
+ (return (+ "import lispy\n\n" pysrc)))
100
107
 
101
- (def l2py-f [file-path :include-meta False]
108
+ (def l2py-f [file-path :include-meta False :no-lispy False]
102
109
  "Translate lispy file to Python source string (standalone with sys.path setup)"
103
110
  (= script-dir (osp.dirname (osp.abspath file-path)))
104
111
  (if (not (in script-dir sys.path))
@@ -118,13 +125,14 @@
118
125
 
119
126
  (with [(open file-path "rb") as f]
120
127
  (= src (.decode (f.read) "utf-8")))
121
- (return (+ (l2py-s src :include-meta include_meta :fresh-scope True) "\n")))
128
+ (= pkg (detect-package-name :path file-path))
129
+ (return (+ (l2py-s src :include-meta include_meta :fresh-scope True :package pkg :no-lispy no_lispy) "\n")))
122
130
 
123
131
  (def l2py []
124
132
  "CLI entry point: parse args and translate file"
125
133
  (= [args more-args] (argparser.parse-known-args))
126
134
  (= file (osp.join (os.getcwd) (sub more-args 0)))
127
- (print (l2py-f file :include-meta args.include_meta)))
135
+ (print (l2py-f file :include-meta args.include_meta :no-lispy args.no_lispy)))
128
136
 
129
137
  (def read-multiline-input [prompt_str]
130
138
  (= paren-depth 0)
@@ -170,9 +178,13 @@
170
178
  )
171
179
 
172
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))
173
185
  (= scope SCOPE)
174
186
  (eval-and-print "(require lispy.macros *)
175
- (from pprint [pprint])")
187
+ (from pprint [pprint])" scope)
176
188
  (while True
177
189
  (try
178
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.2.1"
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