pitybas 0.1.0__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 (33) hide show
  1. pitybas-0.1.0/LICENSE +19 -0
  2. pitybas-0.1.0/PKG-INFO +67 -0
  3. pitybas-0.1.0/README.md +38 -0
  4. pitybas-0.1.0/pyproject.toml +52 -0
  5. pitybas-0.1.0/setup.cfg +4 -0
  6. pitybas-0.1.0/src/pitybas/__init__.py +1 -0
  7. pitybas-0.1.0/src/pitybas/__main__.py +4 -0
  8. pitybas-0.1.0/src/pitybas/cli.py +68 -0
  9. pitybas-0.1.0/src/pitybas/common.py +41 -0
  10. pitybas-0.1.0/src/pitybas/expression.py +285 -0
  11. pitybas-0.1.0/src/pitybas/interpret.py +269 -0
  12. pitybas-0.1.0/src/pitybas/io/__init__.py +0 -0
  13. pitybas-0.1.0/src/pitybas/io/simple.py +71 -0
  14. pitybas-0.1.0/src/pitybas/io/vt100.py +286 -0
  15. pitybas-0.1.0/src/pitybas/parse.py +367 -0
  16. pitybas-0.1.0/src/pitybas/tokens.py +1356 -0
  17. pitybas-0.1.0/src/pitybas.egg-info/PKG-INFO +67 -0
  18. pitybas-0.1.0/src/pitybas.egg-info/SOURCES.txt +31 -0
  19. pitybas-0.1.0/src/pitybas.egg-info/dependency_links.txt +1 -0
  20. pitybas-0.1.0/src/pitybas.egg-info/entry_points.txt +2 -0
  21. pitybas-0.1.0/src/pitybas.egg-info/requires.txt +3 -0
  22. pitybas-0.1.0/src/pitybas.egg-info/top_level.txt +1 -0
  23. pitybas-0.1.0/tests/test_bas_programs.py +56 -0
  24. pitybas-0.1.0/tests/test_cli.py +184 -0
  25. pitybas-0.1.0/tests/test_common.py +30 -0
  26. pitybas-0.1.0/tests/test_control_flow.py +76 -0
  27. pitybas-0.1.0/tests/test_expressions.py +68 -0
  28. pitybas-0.1.0/tests/test_interpreter_introspection.py +101 -0
  29. pitybas-0.1.0/tests/test_io.py +61 -0
  30. pitybas-0.1.0/tests/test_io_simple.py +131 -0
  31. pitybas-0.1.0/tests/test_known_bugs.py +136 -0
  32. pitybas-0.1.0/tests/test_parser.py +76 -0
  33. pitybas-0.1.0/tests/test_variables.py +73 -0
pitybas-0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2014 Ryan Hileman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
pitybas-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: pitybas
3
+ Version: 0.1.0
4
+ Summary: A TI-BASIC interpreter written in Python
5
+ Author: Ryan Hileman
6
+ Maintainer-email: Thomas Roten <tsr@thomasroten.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/tsroten/pitybas
9
+ Project-URL: Repository, https://github.com/tsroten/pitybas
10
+ Project-URL: Issues, https://github.com/tsroten/pitybas/issues
11
+ Keywords: ti-basic,interpreter,calculator,ti-83,ti-84
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Interpreters
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest; extra == "test"
28
+ Dynamic: license-file
29
+
30
+ pitybas
31
+ =======
32
+ A working TI-BASIC interpreter, written in Python.
33
+
34
+ ## Installation
35
+
36
+ pip install pitybas
37
+
38
+ ## Usage
39
+
40
+ Use `pb -i vt100` to run programs which need a working home screen.
41
+
42
+ If you run `pb` with no filename, it launches an interactive shell.
43
+
44
+ Usage: pb [options] filename
45
+
46
+ Options:
47
+ -h, --help show this help message and exit
48
+ -a, --ast parse, print ast, and quit
49
+ -d, --dump dump variables in stacktrace
50
+ -s, --stacktrace always stacktrace
51
+ -v, --verbose verbose output
52
+ -i IO, --io=IO select an IO system: simple (default), vt100
53
+
54
+ You can also run it as a module without installing the console script:
55
+
56
+ python -m pitybas -i vt100
57
+
58
+ ## Known Limitations
59
+
60
+ - **Graph screen functions are not supported.** Commands that draw to the TI-83/84 graph screen (e.g. `Circle`, `Line`, `DrawF`) are not implemented. Programs that use them will fail or produce no output.
61
+
62
+ ## Development
63
+
64
+ Clone the repository and install it in editable mode with the test extras:
65
+
66
+ pip install -e .[test]
67
+ pytest
@@ -0,0 +1,38 @@
1
+ pitybas
2
+ =======
3
+ A working TI-BASIC interpreter, written in Python.
4
+
5
+ ## Installation
6
+
7
+ pip install pitybas
8
+
9
+ ## Usage
10
+
11
+ Use `pb -i vt100` to run programs which need a working home screen.
12
+
13
+ If you run `pb` with no filename, it launches an interactive shell.
14
+
15
+ Usage: pb [options] filename
16
+
17
+ Options:
18
+ -h, --help show this help message and exit
19
+ -a, --ast parse, print ast, and quit
20
+ -d, --dump dump variables in stacktrace
21
+ -s, --stacktrace always stacktrace
22
+ -v, --verbose verbose output
23
+ -i IO, --io=IO select an IO system: simple (default), vt100
24
+
25
+ You can also run it as a module without installing the console script:
26
+
27
+ python -m pitybas -i vt100
28
+
29
+ ## Known Limitations
30
+
31
+ - **Graph screen functions are not supported.** Commands that draw to the TI-83/84 graph screen (e.g. `Circle`, `Line`, `DrawF`) are not implemented. Programs that use them will fail or produce no output.
32
+
33
+ ## Development
34
+
35
+ Clone the repository and install it in editable mode with the test extras:
36
+
37
+ pip install -e .[test]
38
+ pytest
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pitybas"
7
+ dynamic = ["version"]
8
+ description = "A TI-BASIC interpreter written in Python"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ { name = "Ryan Hileman" },
14
+ ]
15
+ maintainers = [
16
+ { name = "Thomas Roten", email = "tsr@thomasroten.com" },
17
+ ]
18
+ requires-python = ">=3.9"
19
+ keywords = ["ti-basic", "interpreter", "calculator", "ti-83", "ti-84"]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Environment :: Console",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Software Development :: Interpreters",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/tsroten/pitybas"
36
+ Repository = "https://github.com/tsroten/pitybas"
37
+ Issues = "https://github.com/tsroten/pitybas/issues"
38
+
39
+ [project.scripts]
40
+ pb = "pitybas.cli:main"
41
+
42
+ [project.optional-dependencies]
43
+ test = ["pytest"]
44
+
45
+ [tool.setuptools.dynamic]
46
+ version = { attr = "pitybas.__version__" }
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from pitybas.cli import main
2
+
3
+ if __name__ == '__main__':
4
+ main()
@@ -0,0 +1,68 @@
1
+ import sys
2
+ import traceback
3
+ from optparse import OptionParser
4
+
5
+ from .interpret import Interpreter, Repl
6
+ from .common import Error
7
+ from pitybas.io.vt100 import IO as vt100
8
+
9
+
10
+ def main(argv=None):
11
+ parser = OptionParser(usage='Usage: pb [options] filename')
12
+ parser.add_option('-a', '--ast', dest="ast", action="store_true", help="parse, print ast, and quit")
13
+ parser.add_option('-d', '--dump', dest="vardump", action="store_true", help="dump variables in stacktrace")
14
+ parser.add_option('-s', '--stacktrace', dest="stacktrace", action="store_true", help="always stacktrace")
15
+ parser.add_option('-v', '--verbose', dest="verbose", action="store_true", help="verbose output")
16
+ parser.add_option('-i', '--io', dest="io", help="select an IO system: simple (default), vt100")
17
+
18
+ (options, args) = parser.parse_args(argv)
19
+
20
+ if len(args) > 1:
21
+ parser.print_help()
22
+ sys.exit(1)
23
+
24
+ io = None
25
+ if options.io == 'vt100':
26
+ io = vt100
27
+
28
+ if args:
29
+ vm = Interpreter.from_file(args[0], history=20, io=io)
30
+ else:
31
+ print('Welcome to pitybas. Press Ctrl-D to exit.')
32
+ print()
33
+ vm = Repl(history=20, io=io)
34
+
35
+ if options.verbose:
36
+ vm.print_tokens()
37
+ print()
38
+ if args:
39
+ print('-===[ Running %s ]===-' % args[0])
40
+
41
+ if options.ast:
42
+ vm.print_ast()
43
+ sys.exit(0)
44
+
45
+ try:
46
+ vm.execute()
47
+ if options.stacktrace:
48
+ vm.print_stacktrace(options.vardump)
49
+ except KeyboardInterrupt:
50
+ print()
51
+ vm.print_stacktrace(options.vardump)
52
+ except Exception as e:
53
+ print()
54
+ print()
55
+ vm.print_stacktrace(options.vardump)
56
+
57
+ print('%s on line %i:' % (e.__class__.__name__, vm.line), end=' ')
58
+
59
+ if isinstance(e, Error):
60
+ print(e.msg)
61
+ else:
62
+ print()
63
+ print('-===[ Python traceback ]===-')
64
+ print(traceback.format_exc())
65
+
66
+
67
+ if __name__ == '__main__':
68
+ main()
@@ -0,0 +1,41 @@
1
+ class Error(Exception):
2
+ def __init__(self, msg):
3
+ self.msg = msg
4
+
5
+ def __str__(self):
6
+ return self.msg
7
+
8
+ class StopError(Exception): pass
9
+ class ReturnError(Exception): pass
10
+
11
+ class ParseError(Error): pass
12
+ class ExecutionError(Error): pass
13
+ class ExpressionError(Error): pass
14
+
15
+ class Pri:
16
+ # evaluation happens in the following order:
17
+ # skip: expressions, functions, variables
18
+ # 1. exponents, factorials
19
+ # 2. multiplication, division
20
+ # 3. addition, subtraction
21
+ # 4. logic operators
22
+ # 5. boolean operators
23
+ # 6. variable setting
24
+
25
+ # these won't be parsed into expressions at all
26
+ INVALID = -2
27
+ # NONE means store but don't execute directly
28
+ # used for variables, lazy loading functions and expressions
29
+ NONE = -1
30
+
31
+ PROB = 0
32
+ EXPONENT = 1
33
+ MULTDIV = 2
34
+ ADDSUB = 3
35
+
36
+ LOGIC = 4
37
+ BOOL = 5
38
+ SET = 6
39
+
40
+ def is_number(num):
41
+ return str(num).lstrip('-').replace('.', '', 1).isdigit()
@@ -0,0 +1,285 @@
1
+ from . import tokens
2
+ from .common import ExpressionError, Pri, is_number
3
+
4
+ class Base:
5
+ priority = Pri.NONE
6
+
7
+ can_run = False
8
+ can_set = False
9
+ can_get = True
10
+ can_fill_left = False
11
+ can_fill_right = False
12
+ absorbs = ()
13
+
14
+ end = None
15
+
16
+ def __init__(self, *elements):
17
+ self.contents = []
18
+ self.raw = []
19
+ self.finished = False
20
+
21
+ for e in elements:
22
+ self.append(e)
23
+
24
+ def append(self, token):
25
+ if self.contents:
26
+ prev = self.contents[-1]
27
+
28
+ # the minus sign implies a * -1 when used by itself
29
+ if isinstance(prev, tokens.Minus):
30
+ # TODO: fix this the rest of the way
31
+ if len(self.contents) == 1:
32
+ self.contents.pop()
33
+ self.contents += [tokens.Value(-1), tokens.Mult()]
34
+
35
+ # absorb: tokens can absorb the next token from the expression if it matches a list of types
36
+ elif isinstance(token, prev.absorbs):
37
+ if isinstance(token, Base):
38
+ token = token.flatten()
39
+
40
+ prev.absorb(token)
41
+ return
42
+
43
+ # implied multiplication
44
+ elif prev.priority == token.priority == tokens.Pri.NONE:
45
+
46
+ # negative numbers actually have implied addition
47
+ if isinstance(token, tokens.Value)\
48
+ and is_number(token.value) and int(token.value) < 0:
49
+ self.contents.append(tokens.Plus())
50
+ else:
51
+ self.contents.append(tokens.Mult())
52
+
53
+ self.raw.append(token)
54
+ self.contents.append(token)
55
+
56
+ def extend(self, array):
57
+ for x in array:
58
+ self.append(x)
59
+
60
+ def flatten(self):
61
+ if len(self.contents) == 1:
62
+ first = self.contents[0]
63
+ if isinstance(first, Base):
64
+ return first.flatten()
65
+ elif first.can_get:
66
+ return first
67
+
68
+ return self
69
+
70
+ def fill(self):
71
+ # TODO: instead of this system, perhaps tokens should be able to specify whether they need/want left/right params
72
+ if not self.contents: return
73
+
74
+ # if we don't have a proper variable:token:variable pairing in the token list,
75
+ # this method will allow tokens to fill in an implied variable to their left or right
76
+ new = []
77
+ for i in range(len(self.contents)):
78
+ t = self.contents[i]
79
+ if (i % 2 == 0 and not t.can_get):
80
+ left = None
81
+ right = None
82
+
83
+ if i > 0:
84
+ left = self.contents[i-1]
85
+ if not left.can_fill_right:
86
+ left = None
87
+
88
+ right = self.contents[i]
89
+ if not right.can_fill_left:
90
+ right = None
91
+
92
+ if left is not None and right is not None:
93
+ if left < right:
94
+ left = None
95
+ else:
96
+ right = None
97
+
98
+ if left is not None:
99
+ new.append(left.fill_right())
100
+ elif right is not None:
101
+ new.append(right.fill_left())
102
+
103
+ new.append(t)
104
+
105
+ last = new[-1]
106
+ if not last.can_get:
107
+ if last.can_fill_right:
108
+ new.append(last.fill_right())
109
+
110
+ self.contents = new
111
+
112
+ def validate(self):
113
+ if not self.contents: return
114
+
115
+ # figure out how to handle in-place tokens like the symbol for ^3
116
+ # perhaps replace it with a ^3 so we can enforce (value, token, value)
117
+ # or we can pad "in-place" tokens with a null to be passed as right
118
+
119
+ # make sure expression is ordered (value, token, value, token, value)
120
+ for i in range(len(self.contents)):
121
+ t = self.contents[i]
122
+
123
+ if (i % 2 == 0 and not t.can_get) or ( i % 2 == 1 and not t.can_run):
124
+ raise ExpressionError('bad token order: %s' % self)
125
+
126
+ # determine whether we have any tokens after a ->
127
+ found_stor = False
128
+ for i in range(len(self.contents)):
129
+ t = self.contents[i]
130
+ odd = i % 2
131
+
132
+ if isinstance(t, tokens.Store):
133
+ found_stor = True
134
+ stor_odd = odd
135
+ elif found_stor and (odd == stor_odd):
136
+ raise ExpressionError('Store cannot be followed by non-Store tokens in expression: %s' % self)
137
+
138
+ def order(self):
139
+ # this step returns a list of ordered indicies
140
+ # to help reduce tokens to a single value
141
+ # see common.Pri for an ordering explanation
142
+
143
+ order = {}
144
+
145
+ for i in range(len(self.contents)):
146
+ token = self.contents[i]
147
+ p = token.priority
148
+ if p >= 0:
149
+ # anything below zero is to be ignored
150
+ if p in order:
151
+ order[p].append(i)
152
+ else:
153
+ order[p] = [i]
154
+
155
+ ret = []
156
+ for p in sorted(order):
157
+ ret += order[p]
158
+
159
+ return ret
160
+
161
+ def get(self, vm):
162
+ self.fill()
163
+ self.validate()
164
+
165
+ sub = []
166
+ expr = self.contents[:]
167
+ for i in self.order():
168
+ n = 0
169
+ for s in sub:
170
+ if s < i:
171
+ n += 1
172
+
173
+ sub += [i, i+1]
174
+ i -= n
175
+
176
+ right = expr.pop(i+1)
177
+ left = expr.pop(i-1)
178
+
179
+ token = expr[i-1]
180
+ expr[i-1] = tokens.Value(token.run(vm, left, right))
181
+
182
+ return vm.get(expr[0])
183
+
184
+ def finish(self):
185
+ self.finished = True
186
+
187
+ def close(self, char):
188
+ for stack in reversed(self.contents):
189
+ if isinstance(stack, Base):
190
+ if stack.close(char):
191
+ return False
192
+
193
+ if char == self.end and not self.finished:
194
+ self.finish()
195
+ return True
196
+
197
+ def __str__(self):
198
+ return ''.join([a.token for a in self.raw])
199
+
200
+ def __len__(self):
201
+ return len(self.contents)
202
+
203
+ def __repr__(self):
204
+ return 'E(%s)' % (' '.join(repr(token) for token in self.contents))
205
+
206
+ bracket_map = {'(':')', '{':'}', '[':']'}
207
+
208
+ class Expression(Base):
209
+ def set(self, vm, value):
210
+ if len(self.contents) == 1:
211
+ self.contents[0].set(vm, value)
212
+
213
+ class Bracketed(Base):
214
+ def __init__(self, end):
215
+ self.end = bracket_map[end]
216
+ Base.__init__(self)
217
+
218
+ def __repr__(self):
219
+ return 'B(%s)' % (' '.join(repr(token) for token in self.contents))
220
+
221
+ class ParenExpr(Bracketed):
222
+ end = ')'
223
+
224
+ class Tuple(Base):
225
+ priority = Pri.INVALID
226
+
227
+ def __init__(self):
228
+ Base.__init__(self)
229
+
230
+ def append(self, expr):
231
+ if isinstance(expr, Base):
232
+ expr = expr.flatten()
233
+
234
+ if self.contents:
235
+ last = self.contents[-1]
236
+ if isinstance(last, Base) and not last.finished:
237
+ last.append(expr)
238
+ return
239
+
240
+ expr = Expression(expr)
241
+ self.contents.append(expr)
242
+
243
+ def sep(self):
244
+ if self.contents:
245
+ self.contents[-1].finish()
246
+
247
+ def get(self, vm):
248
+ return [vm.get(arg) for arg in self.contents]
249
+
250
+ def __len__(self):
251
+ return len(self.contents)
252
+
253
+ def __repr__(self):
254
+ def expr_repr(e):
255
+ if not isinstance(e, Expression):
256
+ return '({})'.format(e)
257
+ else:
258
+ return repr(e)
259
+
260
+ return 'T(%s)' % (', '.join(expr_repr(expr) for expr in self.contents))
261
+
262
+ class Arguments(Tuple, Bracketed):
263
+ def __init__(self, end):
264
+ Bracketed.__init__(self, end)
265
+
266
+ def __repr__(self):
267
+ return 'A(%s)' % (', '.join(repr(expr) for expr in self.contents))
268
+
269
+ class FunctionArgs(Arguments):
270
+ end = ')'
271
+
272
+ class ListExpr(Arguments):
273
+ priority = Pri.NONE
274
+ end = '}'
275
+
276
+ def __repr__(self):
277
+ return 'L{%s}' % (', '.join(repr(expr) for expr in self.contents))
278
+
279
+ class MatrixExpr(Arguments):
280
+ priority = Pri.NONE
281
+ end = ']'
282
+
283
+ def __repr__(self):
284
+ return 'M[%s]' % (', '.join(repr(expr) for expr in self.contents))
285
+