linked-data-python 0.0.4__py3-none-any.whl → 0.2.0__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.
Files changed (41) hide show
  1. ldpy/__init__.py +35 -11
  2. ldpy/__main__.py +75 -250
  3. ldpy/build.py +91 -0
  4. ldpy/console.py +116 -0
  5. ldpy/debug.py +235 -0
  6. ldpy/formatter.py +329 -0
  7. ldpy/importer.py +110 -0
  8. ldpy/lsp/__init__.py +11 -0
  9. ldpy/lsp/__main__.py +4 -0
  10. ldpy/lsp/backend.py +118 -0
  11. ldpy/lsp/rpc.py +104 -0
  12. ldpy/lsp/server.py +353 -0
  13. ldpy/lsp/translate.py +140 -0
  14. ldpy/pygments_lexer.py +613 -0
  15. ldpy/runtime.py +931 -0
  16. ldpy/sparql.py +551 -0
  17. ldpy/transpiler/__init__.py +14 -0
  18. ldpy/transpiler/core.py +2558 -0
  19. ldpy/transpiler/errors.py +38 -0
  20. ldpy/transpiler/linemap.py +292 -0
  21. linked_data_python-0.2.0.dist-info/METADATA +158 -0
  22. linked_data_python-0.2.0.dist-info/RECORD +26 -0
  23. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
  24. linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
  25. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
  26. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
  27. ldpy/grun/lib.py +0 -63
  28. ldpy/grun/util.py +0 -11
  29. ldpy/ldpy.py +0 -183
  30. ldpy/rewriter/IndentedStringWriter.py +0 -54
  31. ldpy/rewriter/LDPythonRewriter.py +0 -677
  32. ldpy/rewriter/MultiChannelTokenStream.py +0 -127
  33. ldpy/rewriter/Result.py +0 -49
  34. ldpy/rewriter/__init__.py +0 -7
  35. ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
  36. ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
  37. ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
  38. ldpy/sparql/builtin.py +0 -326
  39. linked_data_python-0.0.4.dist-info/METADATA +0 -139
  40. linked_data_python-0.0.4.dist-info/RECORD +0 -20
  41. linked_data_python-0.0.4.dist-info/entry_points.txt +0 -3
ldpy/__init__.py CHANGED
@@ -1,11 +1,35 @@
1
- from ldpy.rewriter.IndentedStringWriter import IndentedStringWriter
2
- from ldpy.rewriter.MultiChannelTokenStream import MultiChannelTokenStream
3
- from ldpy.rewriter.antlr.LDPythonLexer import LDPythonLexer
4
- from ldpy.rewriter.antlr.LDPythonParser import LDPythonParser
5
- from ldpy.rewriter.antlr.LDPythonVisitor import LDPythonVisitor
6
- from ldpy.rewriter.LDPythonRewriter import LDPythonRewriter
7
- from ldpy.rewriter.LDPythonRewriter import LDPythonRewriter
8
- from ldpy.ldpy import transform_source, config
9
-
10
- __version__ = "0.0.4"
11
- __date__ = "2023-03-23"
1
+ """Linked-Data Python (ldpy) v2.
2
+
3
+ An "island parsing" transpiler: Python is copied through untouched, the RDF
4
+ islands (@prefix, @base, IRIs, prefixed names, RDF literals, variables,
5
+ graphs g{...}) are rewritten into Python expressions built on ldpy.runtime.
6
+
7
+ The historical ANTLR chain (v1) is still reachable through the ldpy.rewriter
8
+ package but is no longer imported by default.
9
+ """
10
+
11
+ from ldpy.transpiler import transpile, LdpySyntaxError, LdpyWarning
12
+ from ldpy.importer import install, uninstall, install_excepthook
13
+
14
+ __version__ = "0.1.0.dev0"
15
+ __date__ = "2026-08-26"
16
+
17
+
18
+ def transform_source(source, filename="<ldpy>"):
19
+ """Simplified v1 compatibility: returns (python_code, prefixes, map)."""
20
+ result = transpile(source, filename)
21
+ return result.code, result.prefixes, result.map
22
+
23
+
24
+ def Coercion(rules):
25
+ """Politique de conversion Python -> RDF (fiche 020) — voir
26
+ ldpy.runtime.Coercion; exposed here as public API."""
27
+ from ldpy.runtime import Coercion as _C
28
+ return _C(rules)
29
+
30
+
31
+ def instantiateBGP(input, solutionMappings, initialGraph=None):
32
+ """Instantiate a graph template with solution mappings
33
+ (re-export of ldpy.runtime.instantiateBGP, v1 compatibility)."""
34
+ from ldpy.runtime import instantiateBGP as _f
35
+ return _f(input, solutionMappings, initialGraph)
ldpy/__main__.py CHANGED
@@ -1,257 +1,82 @@
1
- #
2
- # The MIT License (MIT)
3
- #
4
- # Copyright (c) 2022 by Maxime Lefrançois
5
- #
6
- # Permission is hereby granted, free of charge, to any person
7
- # obtaining a copy of this software and associated documentation
8
- # files (the "Software"), to deal in the Software without
9
- # restriction, including without limitation the rights to use,
10
- # copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- # copies of the Software, and to permit persons to whom the
12
- # Software is furnished to do so, subject to the following
13
- # conditions:
14
- #
15
- # The above copyright notice and this permission notice shall be
16
- # included in all copies or substantial portions of the Software.
17
- #
18
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
- # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
- # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
- # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
- # OTHER DEALINGS IN THE SOFTWARE.
26
- #
27
- # Project : ldpython-parser; Linked-Data Python to Python/Micropython Rewriter
28
- # https://gitlab.com/coswot/linked-data-python/ldpy
29
- # Developed by : Maxime Lefrançois, maxime.lefrancois@emse.fr
30
- #
31
- """ldpy extends the Python syntax with primitives from the Semantic Web such as namespaces, RDF terms, and RDF graphs.
1
+ """ldpy — command-line execution.
32
2
 
33
- If no source is given, ldpy will start an interactive console.
3
+ Usage :
4
+ python -m ldpy source.ldpy # transpile and run
5
+ python -m ldpy -s source.ldpy # also print the transformed code
6
+ python -m ldpy -t source.ldpy # transpile seulement (stdout)
34
7
  """
8
+
9
+ import argparse
35
10
  import os
36
11
  import sys
37
- import platform
38
- import argparse
39
- import ldpy
40
- from importlib import import_module
41
- import types
42
- from code import InteractiveConsole
43
- import readline
44
-
45
- # include a subset of the arguments from https://github.com/aroberge/ideas/blob/master/ideas/__main__.py
46
- parser = argparse.ArgumentParser(
47
- formatter_class=argparse.RawDescriptionHelpFormatter,
48
- description=__doc__,
49
- )
50
-
51
- parser.add_argument(
52
- "-v",
53
- "--version",
54
- help="only displays the current version.",
55
- action="store_true",
56
- )
57
-
58
- parser.add_argument(
59
- "-l",
60
- '--debug-lexer',
61
- dest='lexer',
62
- action='store_true',
63
- default=False,
64
- help='print the lexer output'
65
- )
66
-
67
- parser.add_argument(
68
- '-p',
69
- '--debug-parser',
70
- dest='parser',
71
- action='store_true',
72
- default=False,
73
- help='print the parser output'
74
- )
75
-
76
- parser.add_argument(
77
- "-s",
78
- "--show_changes",
79
- action="store_true",
80
- help="""shows the transformed code before it is executed.""",
81
- )
82
-
83
- parser.add_argument(
84
- "-i",
85
- action="store_true",
86
- help="""starts the interactive console after executing a source"""
87
- )
88
-
89
- group = parser.add_mutually_exclusive_group(required=False)
90
-
91
- group.add_argument(
92
- "-m",
93
- "--module",
94
- dest="module",
95
- type=str,
96
- help="""Run library module as a script. The module may be a .ldpy or a .py file.""",
97
- )
98
12
 
99
- group.add_argument(
100
- "source",
101
- nargs="?",
102
- help="""Program read from script file. The file extension may be .ldpy or .py.""",
103
- )
104
-
105
- class LdpyConsole(InteractiveConsole):
106
-
107
- def __init__(self, show_changes=False, locals=None):
108
- sys.ps1 = "ldpy> "
109
- sys.ps2 =" ... "
110
- if locals is None:
111
- locals={"__name__": "__console__",
112
- "__doc__": None,
113
- "__base__": None,
114
- "__namespaces__": dict()}
115
- locals["__debug_ldpy__"] = show_changes
116
- InteractiveConsole.__init__(self, locals, filename="<ldpy console>")
117
- self.resetldpybuffer()
118
- self.push("import rdflib")
119
-
120
- def resetldpybuffer(self):
121
- self.ldpybuffer = []
122
-
123
- def push(self, line):
124
- """Push a ldpy line to the interpreter.
125
-
126
- The line should not have a trailing newline; it may have
127
- internal newlines. The line is appended to a buffer and the
128
- interpreter's runsource() method is called with the
129
- concatenated contents of the buffer as source. If this
130
- indicates that the command was executed or invalid, the buffer
131
- is reset; otherwise, the command is incomplete, and the buffer
132
- is left as it was after the line was appended. The return
133
- value is 1 if more input is required, 0 if the line was dealt
134
- with in some way (this is the same as runsource()).
135
-
136
- """
137
- self.ldpybuffer.append(line)
138
- ldpysource = "\n".join(self.ldpybuffer)
139
-
140
- try:
141
- namespaces = self.locals.get("__namespaces__", dict())
142
- transformed_source, namespaces, linemap = ldpy.transform_source(ldpysource, self.filename, namespaces)
143
- self.locals["__namespaces__"] = namespaces
144
- except SyntaxError as ex:
145
- if "mismatched input '<EOF>' expecting" in ex.msg:
146
- return True
147
- self.write(ex.msg + "\n")
148
- self.resetldpybuffer()
149
- return False
150
- self.resetldpybuffer()
151
-
152
- if self.locals["__debug_ldpy__"]:
153
- print("", file=sys.stderr)
154
- for transformed_line in transformed_source.split("\n"):
155
- print("ldpy>>> " + transformed_line, file=sys.stderr)
156
-
157
- for line in transformed_source.split("\n"):
158
- self.buffer.append(line)
159
- source = "\n".join(self.buffer)
160
- more = self.runsource(source, self.filename)
161
- if not more:
162
- self.resetbuffer()
163
- return more
164
-
165
- def interact(self):
166
- from rdflib import __version__ as rdflib_version
167
- BANNER = (
168
- f">>> ldpy interactive console version {ldpy.__version__}. "
169
- + f"[rdflib version {rdflib_version}, "
170
- + f"Python version: {platform.python_version()}]"
171
- )
172
- InteractiveConsole.interact(self, banner=BANNER)
173
-
174
- def rewrite_traceback(tb, linemap):
175
- if not tb:
176
- return None
177
- tb_frame = tb.tb_frame
178
- if "__name__" in tb_frame.f_globals:
179
- name = tb_frame.f_globals["__name__"]
180
- if name == 'importlib' or name == 'importlib._bootstrap' or name == 'ideas.import_hook':
181
- return rewrite_traceback(tb.tb_next, linemap)
182
- tb_lineno = tb.tb_lineno
183
- tb_lasti = tb.tb_lasti
184
- if "__file__" in tb_frame.f_locals and tb_frame.f_locals["__file__"].endswith(".ldpy"):
185
- file = tb_frame.f_locals["__file__"]
186
- if file in linemap:
187
- linemap_for_file = linemap[file]
188
- tb_lineno = linemap_for_file[tb_lineno-4] # -4 because hook source_init contains three lines, and index starts at 0
189
- ## it would be nice to have a second tb with the transformed .py source, but it's not possible to instantiate the builtin Frame type
190
- # one could update the __file__ local attribute as follows
191
- # tb_frame.f_locals["__file__"] = tb_frame.f_locals["__file__"][:-5] + ".py"
192
- # and add the the traceback without changing the lineno
193
- return types.TracebackType(rewrite_traceback(tb.tb_next, linemap), tb_frame, tb_lasti, tb_lineno)
13
+ import ldpy
14
+ from ldpy.transpiler import transpile, LdpySyntaxError
15
+ from ldpy.transpiler.linemap import compile_mapped
16
+
17
+
18
+ def main(argv=None):
19
+ parser = argparse.ArgumentParser(
20
+ prog="ldpy",
21
+ description="ldpy extends Python syntax with Semantic Web "
22
+ "primitives (IRIs, RDF literals, graphs).")
23
+ parser.add_argument("-v", "--version", action="store_true",
24
+ help="print the version and exit.")
25
+ parser.add_argument("-s", "--show-changes", action="store_true",
26
+ help="print the transformed code before running it.")
27
+ parser.add_argument("-t", "--transpile-only", action="store_true",
28
+ help="write the transformed code to stdout, do not run.")
29
+ parser.add_argument("-i", "--interactive", action="store_true",
30
+ help="open the interactive console after the script.")
31
+ parser.add_argument("-m", "--map", action="store_true",
32
+ help="also write the language map (<source>.map).")
33
+ parser.add_argument("source", nargs="?",
34
+ help=".ldpy (or .py) file to run.")
35
+ args = parser.parse_args(argv)
194
36
 
195
- def main() -> None:
196
- args = parser.parse_args()
197
37
  if args.version:
198
- print(f"\nldpy version {ldpy.__version__}")
199
- return
200
- if args.source is None and args.module is None:
201
- """Starts a special console that works with ldpy."""
202
- c = LdpyConsole(show_changes=args.show_changes)
203
- c.interact()
204
-
205
- ldpy.config.debug_ldpy = args.show_changes
206
- ldpy.config.debug_lexer = args.lexer
207
- ldpy.config.debug_parser = args.parser
208
-
209
- if args.source is not None:
210
- filename = args.source
211
- if not (filename.endswith(".py") or filename.endswith(".ldpy")):
212
- raise ValueError("ldpy file argument must end with .py or .ldpy")
213
- with open(args.source) as f:
214
- source = f.read()
215
- if filename.endswith(".ldpy"):
216
- source, namespaces, linemap = ldpy.transform_source(source, filename)
217
- source = "import rdflib\n__base__ = None\n__namespaces__ = dict()\n" + source
218
- directory = os.path.dirname(filename)
219
- ldpyname = os.path.basename(filename)
220
- pyname = os.path.splitext(ldpyname)[0] + ".py"
221
- os.makedirs(os.path.join(directory, "__ldpycache__"), exist_ok=True)
222
- with open(os.path.join(directory, "__ldpycache__", pyname), 'w') as f:
223
- f.write(source)
224
- ldpy.config.linemap[filename] = linemap
225
- try:
226
- code = compile(source, filename, "exec")
227
- _locals = dict(__file__=filename)
228
- exec(code, _locals)
229
- except Exception as exc:
230
- linemap = ldpy.config.linemap
231
- tb = exc.__traceback__
232
- tb = rewrite_traceback(tb, linemap)
233
- raise exc.with_traceback(tb)
234
-
235
-
236
- if args.module is not None:
237
- name = args.module
238
- if "/" in name or name.endswith(".py") or name.endswith(".ldpy"):
239
- raise ValueError(f"ldpy argument --module must be a valid module name (path.to.my_script). got '{args.module}'")
240
- try:
241
- module = import_module(name)
242
- _locals = module.__dict__
243
- except ModuleNotFoundError as exc:
244
- print(f"{exc.__class__.__name__}: {exc.msg}", file=sys.stderr)
245
- _locals = dict()
246
- except Exception as exc:
247
- linemap = ldpy.config.linemap
248
- tb = exc.__traceback__
249
- tb = rewrite_traceback(tb, linemap)
250
- raise exc.with_traceback(tb)
251
-
252
- if sys.flags.interactive or args.i:
253
- """Starts a special console that works with ldpy."""
254
- c = LdpyConsole(show_changes=args.show_changes, locals=_locals)
255
- c.interact()
256
-
257
- main()
38
+ print("ldpy " + ldpy.__version__)
39
+ return 0
40
+ if not args.source:
41
+ from ldpy.console import interact
42
+ interact()
43
+ return 0
44
+
45
+ with open(args.source, "r", encoding="utf-8") as f:
46
+ source = f.read()
47
+ try:
48
+ result = transpile(source, args.source)
49
+ except LdpySyntaxError as e:
50
+ print(str(e), file=sys.stderr)
51
+ return 1
52
+ for w in result.warnings:
53
+ print(str(w), file=sys.stderr)
54
+ if args.map:
55
+ with open(args.source + ".map", "w", encoding="utf-8") as f:
56
+ f.write(result.map.to_json(indent=1))
57
+ if args.transpile_only:
58
+ sys.stdout.write(result.code)
59
+ return 0
60
+ if args.show_changes:
61
+ print("ldpy>>> ======== transformed code ========", file=sys.stderr)
62
+ for lineno, line in enumerate(result.code.split("\n"), 1):
63
+ print("ldpy>>> %3d: %s" % (lineno, line), file=sys.stderr)
64
+ print("ldpy>>> =================================", file=sys.stderr)
65
+
66
+ ldpy.install()
67
+ from ldpy.importer import MAPS
68
+ src_path = os.path.abspath(args.source)
69
+ MAPS[args.source] = MAPS[src_path] = result.map
70
+ # remapped compilation: tracebacks, pdb and debugpy all speak in
71
+ # .ldpy coordinates (record ldpy/011)
72
+ code = compile_mapped(result.code, result.map, src_path)
73
+ g = {"__name__": "__main__", "__file__": src_path}
74
+ exec(code, g)
75
+ if args.interactive:
76
+ from ldpy.console import interact
77
+ interact(locals=g, prefixes=result.prefixes, base=result.base)
78
+ return 0
79
+
80
+
81
+ if __name__ == "__main__":
82
+ sys.exit(main())
ldpy/build.py ADDED
@@ -0,0 +1,91 @@
1
+ """Materialisation: transpile .ldpy files into a shadow directory.
2
+
3
+ `python -m ldpy.build src/ -o .ldpy-build` mirrors the tree and writes:
4
+ - <module>.py (generated code)
5
+ - <module>.ldpy.map (language map JSON)
6
+
7
+ This is the base of debugging (debugpy runs on the shadow .py files) and of
8
+ language server (voir docs/explanation/tooling.md)."""
9
+
10
+ import os
11
+ import sys
12
+ import argparse
13
+
14
+ from ldpy.transpiler import transpile, LdpySyntaxError
15
+
16
+ DEFAULT_OUT = ".ldpy-build"
17
+
18
+
19
+ def build_file(src_path, out_dir, rel=None):
20
+ """Transpile one file; returns (py_path, map_path, result)."""
21
+ with open(src_path, "r", encoding="utf-8") as f:
22
+ source = f.read()
23
+ rel = rel or os.path.basename(src_path)
24
+ stem = rel[:-5] if rel.endswith(".ldpy") else rel
25
+ py_path = os.path.join(out_dir, stem + ".py")
26
+ map_path = os.path.join(out_dir, stem + ".ldpy.map")
27
+ result = transpile(source, src_path)
28
+ result.map.generated_name = py_path
29
+ os.makedirs(os.path.dirname(py_path) or ".", exist_ok=True)
30
+ with open(py_path, "w", encoding="utf-8") as f:
31
+ f.write(result.code)
32
+ with open(map_path, "w", encoding="utf-8") as f:
33
+ f.write(result.map.to_json(indent=1))
34
+ # Source Map v3: for standard tooling
35
+ with open(py_path + ".map", "w", encoding="utf-8") as f:
36
+ f.write(result.map.to_sourcemap_v3_json())
37
+ return py_path, map_path, result
38
+
39
+
40
+ def build_tree(root, out_dir):
41
+ """Recursively transpile every .ldpy under root. Plain .py files are
42
+ copied as they are (a mixed package must stay importable)."""
43
+ built, errors = [], []
44
+ for dirpath, dirnames, filenames in os.walk(root):
45
+ dirnames[:] = [d for d in dirnames
46
+ if d not in (DEFAULT_OUT, "__pycache__", ".git")]
47
+ for name in filenames:
48
+ src = os.path.join(dirpath, name)
49
+ rel = os.path.relpath(src, root)
50
+ if name.endswith(".ldpy"):
51
+ try:
52
+ built.append(build_file(src, out_dir, rel))
53
+ except LdpySyntaxError as e:
54
+ errors.append(e)
55
+ elif name.endswith(".py"):
56
+ dst = os.path.join(out_dir, rel)
57
+ os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
58
+ with open(src, "rb") as fi, open(dst, "wb") as fo:
59
+ fo.write(fi.read())
60
+ return built, errors
61
+
62
+
63
+ def main(argv=None):
64
+ parser = argparse.ArgumentParser(
65
+ prog="ldpy.build",
66
+ description="Transpile .ldpy files into a shadow directory "
67
+ "(.py + .ldpy.map).")
68
+ parser.add_argument("source", help=".ldpy file or directory")
69
+ parser.add_argument("-o", "--out", default=DEFAULT_OUT,
70
+ help="output directory (default: %(default)s)")
71
+ args = parser.parse_args(argv)
72
+
73
+ if os.path.isdir(args.source):
74
+ built, errors = build_tree(args.source, args.out)
75
+ for e in errors:
76
+ print(str(e), file=sys.stderr)
77
+ print("%d file(s) transpiled into %s" % (len(built), args.out))
78
+ return 1 if errors else 0
79
+ try:
80
+ py_path, _, result = build_file(args.source, args.out)
81
+ except LdpySyntaxError as e:
82
+ print(str(e), file=sys.stderr)
83
+ return 1
84
+ for w in result.warnings:
85
+ print(str(w), file=sys.stderr)
86
+ print(py_path)
87
+ return 0
88
+
89
+
90
+ if __name__ == "__main__":
91
+ sys.exit(main())
ldpy/console.py ADDED
@@ -0,0 +1,116 @@
1
+ """Console interactive Linked-Data Python .
2
+
3
+ The point of the `ideas` package in v1 was to enter the interpreter and type
4
+ ldpy directly. This console does it without `ideas`: every entry is
5
+ transpiled then compiled; the state of top-level @prefix/@base persists from
6
+ one entry to the next (declarations made inside a block die with the entry —
7
+ block scope obliges).
8
+
9
+ $ python -m ldpy # console
10
+ $ python -m ldpy -i script.ldpy # run, then open the console
11
+ """
12
+
13
+ import atexit
14
+ import code
15
+ import codeop
16
+ import os
17
+ import sys
18
+
19
+ import ldpy
20
+ from ldpy.transpiler import LdpySyntaxError
21
+ from ldpy.transpiler.core import Transpiler, PRELUDE
22
+
23
+ HISTORY_FILE = os.path.join(os.path.expanduser("~"), ".ldpy_history")
24
+ HISTORY_LENGTH = 1000
25
+
26
+
27
+ def _setup_readline(locals):
28
+ """Line editing (arrows, Ctrl-A/E…), persistent history and Tab
29
+ completion on console names. A no-op if the readline module is missing
30
+ (Windows without pyreadline)."""
31
+ try:
32
+ import readline
33
+ import rlcompleter
34
+ except ImportError:
35
+ return
36
+ readline.set_completer(rlcompleter.Completer(locals).complete)
37
+ if "libedit" in (getattr(readline, "__doc__", "") or ""):
38
+ readline.parse_and_bind("bind ^I rl_complete") # macOS libedit
39
+ else:
40
+ readline.parse_and_bind("tab: complete")
41
+ try:
42
+ readline.read_history_file(HISTORY_FILE)
43
+ except OSError:
44
+ pass
45
+ readline.set_history_length(HISTORY_LENGTH)
46
+
47
+ def _save():
48
+ try:
49
+ readline.write_history_file(HISTORY_FILE)
50
+ except OSError:
51
+ pass
52
+ atexit.register(_save)
53
+
54
+ BANNER = ("ldpy %s — console Linked-Data Python (Python %s)\n"
55
+ "RDF islands are accepted: @prefix, <iri>, ex:name, g{ ... }, ?v")
56
+
57
+
58
+ class LdpyConsole(code.InteractiveConsole):
59
+ """Interactive console: transpile every entry before compiling it;
60
+ top-level @prefix/@base persist between entries."""
61
+
62
+ def __init__(self, locals=None, filename="<console>",
63
+ prefixes=None, base=None):
64
+ if locals is None:
65
+ locals = {"__name__": "__console__", "__doc__": None}
66
+ super().__init__(locals=locals, filename=filename)
67
+ # the runtime prelude is installed once and for all
68
+ if "__namespaces__" not in self.locals:
69
+ exec(PRELUDE, self.locals)
70
+ self._prefixes = dict(prefixes or {})
71
+ self._prefix_cols = {k: 0 for k in self._prefixes}
72
+ self._base = base
73
+
74
+ def runsource(self, source, filename=None, symbol="single"):
75
+ """Transpile then compile one entry; True = incomplete entry
76
+ (unclosed island or Python block), False = handled."""
77
+ filename = filename or self.filename
78
+ t = Transpiler(source, filename, emit_prelude=False)
79
+ t.prefixes = dict(self._prefixes)
80
+ t._prefix_col = dict(self._prefix_cols)
81
+ t.base = self._base
82
+ try:
83
+ result = t.run()
84
+ except LdpySyntaxError as e:
85
+ if getattr(e, "at_eof", False):
86
+ return True # unclosed island: wait for more
87
+ self.write(str(e) + "\n")
88
+ return False
89
+ try:
90
+ code_obj = codeop.compile_command(result.code, filename, symbol)
91
+ except (SyntaxError, ValueError, OverflowError):
92
+ self.showsyntaxerror(filename)
93
+ return False
94
+ if code_obj is None:
95
+ return True # Python incomplet (def, if, ...)
96
+ # the entry is complete: top-level declarations persist
97
+ t._unwind_scopes(0)
98
+ self._prefixes = dict(t.prefixes)
99
+ self._prefix_cols = dict(t._prefix_col)
100
+ self._base = t.base
101
+ for w in result.warnings:
102
+ self.write(str(w) + "\n")
103
+ self.runcode(code_obj)
104
+ return False
105
+
106
+
107
+ def interact(locals=None, prefixes=None, base=None):
108
+ """Open the ldpy console (banner, Ctrl-D to leave)."""
109
+ console = LdpyConsole(locals=locals, prefixes=prefixes, base=base)
110
+ _setup_readline(console.locals)
111
+ banner = BANNER % (ldpy.__version__, sys.version.split()[0])
112
+ try:
113
+ console.interact(banner=banner, exitmsg="")
114
+ except SystemExit:
115
+ pass
116
+ return console