Skip to content

Commit 3c5f87e

Browse files
committed
Merge branch 'imports'
2 parents 0a17055 + 34cb049 commit 3c5f87e

File tree

13 files changed

+161
-32
lines changed

13 files changed

+161
-32
lines changed

Examples/imports.owpy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#import "lib/child"
2+
3+
Rule "Debug Import"
4+
Actions
5+
debug_func("Hello!")

Examples/lib/child.owpy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#import 'lib2/child2.owpy'
2+
3+
%debug_func(text)
4+
Msg(Everyone, text)
5+
nested(Lucio)

Examples/lib/lib2/child2.owpy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
%nested(hero_)
2+
Set Hero(Event Player, hero_)

OWScript.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import os
23
import re
34
import sys
45
import time
@@ -7,7 +8,7 @@
78
from OWScript.Parser import Parser
89
from OWScript.Transpiler import Transpiler
910

10-
def transpile(text, args):
11+
def transpile(text, path, args):
1112
"""Transpiles an OWScript code into Overwatch Workshop rules."""
1213
start = time.time()
1314
Errors.TEXT = text
@@ -23,7 +24,7 @@ def transpile(text, args):
2324
tree = parser.script()
2425
if args.tree:
2526
print(tree.string())
26-
transpiler = Transpiler(tree=tree)
27+
transpiler = Transpiler(tree=tree, path=path)
2728
code = transpiler.run()
2829
if args.min:
2930
code = re.sub(r'[\s\n]*', '', code)
@@ -50,10 +51,15 @@ def transpile(text, args):
5051
parser.add_argument('--tokens', action='store_true', help='Debug: shows the tokens created by the lexer')
5152
parser.add_argument('--tree', action='store_true', help='Debug: visualizes the AST generated by the parser')
5253
args = parser.parse_args()
53-
file_input = args.input[0] if args.input else sys.stdin
54-
with open(file_input) as f:
55-
text = f.read()
54+
if args.input:
55+
file_input = args.input[0]
56+
path = os.path.abspath(file_input)
57+
with open(path) as f:
58+
text = f.read()
59+
else:
60+
text = sys.stdin.read()
61+
path = os.getcwd()
5662
try:
57-
transpile(text, args=args)
63+
transpile(text, path=path, args=args)
5864
except Errors.OWSError as ex:
5965
print('Error:', ex)

OWScript/AST.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ def __init__(self, code):
9494
def __repr__(self):
9595
return '<Raw {}>'.format(len(self.code))
9696

97+
class Import(AST):
98+
def __init__(self, path):
99+
self.path = path
100+
101+
def __repr__(self):
102+
return '#import {}'.format(self.path)
103+
97104
# Workshop Types
98105
class WorkshopType(AST):
99106
@classmethod

OWScript/Errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class LexError(OWSError):
1616
class ParseError(OWSError):
1717
pass
1818

19+
class ImportError(OWSError):
20+
pass
21+
1922
class SyntaxError(OWSError):
2023
pass
2124

OWScript/Importer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from . import Errors
2+
from .Lexer import Lexer
3+
from .Parser import Parser
4+
5+
def import_file(path):
6+
error_text = Errors.TEXT
7+
with open(path) as f:
8+
text = f.read()
9+
try:
10+
Errors.TEXT = text
11+
lexer = Lexer(text=text)
12+
tokens = lexer.lex()
13+
parser = Parser(tokens=tokens)
14+
tree = parser.script()
15+
Errors.TEXT = error_text
16+
return tree
17+
except Exception as ex:
18+
Errors.TEXT = error_text
19+
raise ex

OWScript/Lexer.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import functools
22
import re
33

4-
try:
5-
from . import Errors
6-
from .Tokens import Token, Tokens
7-
except ImportError:
8-
import Errors
9-
from Tokens import Token, Tokens
4+
from . import Errors
5+
from .Tokens import Token, Tokens
106

117
class Lexer:
128
IGNORE = ('WHITESPACE', 'SEMI', 'COMMENT', 'ANNOTATION')

OWScript/Parser.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
from collections import deque
33
from functools import partial
44

5-
try:
6-
from . import Errors
7-
from .AST import *
8-
from .Tokens import ALIASES
9-
from .Workshop import *
10-
except ImportError:
11-
import Errors
12-
from AST import *
5+
from . import Errors
6+
from .AST import *
7+
from .Tokens import ALIASES
8+
from .Workshop import *
139

1410
class Parser:
1511
def __init__(self, tokens):
@@ -105,11 +101,13 @@ def script(self):
105101
return node
106102

107103
def stmt(self):
108-
"""stmt : (funcdef | ruledef | line)"""
104+
"""stmt : (funcdef | ruledef | importdef | line)"""
109105
if self.curvalue == '%':
110106
return self.funcdef()
111107
elif self.curtype in ('DISABLED', 'RULE'):
112108
return self.ruledef()
109+
elif self.curtype == 'IMPORT':
110+
return self.importdef()
113111
else:
114112
return self.line()
115113

@@ -207,6 +205,16 @@ def block(self):
207205
node.children.append(line)
208206
return node
209207

208+
def importdef(self):
209+
"""importdef : #import STRING"""
210+
self.eat('IMPORT')
211+
path = self.curvalue.strip('\'').strip('"').replace('/', '\\').rstrip('.owpy')
212+
pos = self.curpos
213+
self.eat('STRING')
214+
node = Import(path=path)
215+
node._pos = pos
216+
return node
217+
210218
def line(self):
211219
"""line :
212220
( if_stmt

OWScript/Tokens.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class Tokens:
8585
RBRACK : r'\]'
8686
STRING : r'("[^\\\r\n\f]*?"|\'[^\\\r\n\f]*?\')'
8787
F_STRING : r'(`[^\\\r\n\f]*?`)'
88+
IMPORT : r'#IMPORT\b'
8889
IF : r'IF\b'
8990
ELIF : r'ELIF\b'
9091
ELSE : r'ELSE\b'

OWScript/Transpiler.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1+
import os
12
import re
23
from collections import defaultdict
34
from itertools import chain, count
45
from string import capwords, ascii_uppercase as letters
56

6-
try:
7-
from . import Errors
8-
from .AST import *
9-
from .Workshop import Workshop
10-
except ImportError:
11-
from AST import *
7+
from . import Errors
8+
from . import Importer
9+
from .AST import *
10+
from .Workshop import Workshop
1211

1312
def flatten(l):
1413
l = list(l)
@@ -82,8 +81,9 @@ def map_2pos(a, b):
8281
get_map: get_map
8382

8483
class Transpiler:
85-
def __init__(self, tree, indent_size=3):
84+
def __init__(self, tree, path, indent_size=3):
8685
self.tree = tree
86+
self.path = path
8787
self.indent_size = indent_size
8888
self.indent_level = 0
8989
# Reserved Global Indices
@@ -95,6 +95,7 @@ def __init__(self, tree, indent_size=3):
9595
self.player_varconst = iter(letters[1:])
9696
self.varconsts = {}
9797
self.curblock = []
98+
self.imports = set()
9899

99100
@property
100101
def tabs(self):
@@ -126,9 +127,48 @@ def resolve_skips(self):
126127
for skip, jump in zip(skips, skip_to[::-1]):
127128
self.curblock[skip] = self.curblock[skip].format(jump)
128129

130+
def resolve_import(self, node, scope):
131+
# TODO - Resolves imports recursively
132+
children = self.visit(node, scope).children
133+
nodes = []
134+
for child in children:
135+
if type(child) == Import:
136+
cur_path = self.path
137+
self.path = os.path.join(os.path.dirname(self.path), os.path.dirname(node.path)) + '\\'
138+
result = self.resolve_import(child, scope)
139+
self.path = cur_path
140+
nodes = result + nodes
141+
else:
142+
nodes.append(child)
143+
return nodes
144+
129145
def visitScript(self, node, scope):
130146
code = r'rule("Generated by https://github.com/adapap/OWScript") { Event { Ongoing - Global; } Actions { Set Global Variable At Index(A, 0, Round To Integer(Add(Distance Between(Nearest Walkable Position(Vector(-500.000, 0, 0)), Nearest Walkable Position(Vector(500, 0, 0))), Distance Between(Nearest Walkable Position(Vector(0, 0, -500.000)), Nearest Walkable Position(Vector(0, 0, 500)))), Down)); }}' + '\n'
131-
return (code + ''.join(self.visit_children(node, scope))).rstrip('\n')
147+
while len(node.children) > 0:
148+
child = node.children[0]
149+
if type(child) == Import:
150+
node.children = self.resolve_import(child, scope) + node.children[1:]
151+
else:
152+
code += self.visit(child, scope)
153+
node.children = node.children[1:]
154+
return code.rstrip('\n')
155+
156+
def visitImport(self, node, scope):
157+
file_dir = os.path.dirname(self.path)
158+
path = os.path.join(file_dir, node.path) + '.owpy'
159+
try:
160+
assert os.path.exists(path)
161+
except AssertionError:
162+
raise Errors.ImportError('File {} could not be found'.format(node.path), pos=node._pos)
163+
try:
164+
assert path not in self.imports
165+
self.imports.add(path)
166+
result = Importer.import_file(path)
167+
except AssertionError:
168+
print('DEBUG - Skipping duplicate import {}'.format(path))
169+
except Exception as ex:
170+
raise Errors.ImportError('Failed to import \'{}\' due to the following error:\n{}'.format(node.path, ex), pos=node._pos)
171+
return result
132172

133173
def visitRule(self, node, scope):
134174
code = ''
@@ -527,7 +567,7 @@ def visitCall(self, node, scope):
527567
else:
528568
print('DEBUG - Call Origin:', type(parent))
529569
if not base_node:
530-
raise Errors.NameError('Undefined function \'{}\''.format(base_name[5:]), pos=parent._pos)
570+
raise Errors.NameError('Undefined function \'{}\''.format(func_name), pos=parent._pos)
531571
func = base_node if type(base_node) != Variable else base_node.value
532572
if type(func) == Function:
533573
# Assert arity

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Setup
1010
- `-m | --min` Optional: minifies the output by stripping whitespace
1111
- `-s | --save [FILE]` Optional: saves to the target output file instead of stdout
1212
- `-c | --copy` Optional: copies code to clipboard (must have *pyperclip* installed: `pip install pyperclip`)
13+
- `--enable-imports` Optional: Enables the experimental import functionality.
1314

1415
## Syntax Highlighting
1516
In the `Syntax/` folder, you can find the raw Iro code which I used to generate a Sublime Text file with modifications. You can directly import the `OWScript.sublime-syntax` file by putting it in your ST3 `User` folder.
@@ -30,6 +31,7 @@ Documentation
3031
* [Functions](#functions)
3132
* [Loops](#loops)
3233
* [Attributes / Methods](#attributes--methods)
34+
* [Imports](#imports)
3335

3436
**Data Types & Structures**
3537
* [Variables](#variables)
@@ -286,4 +288,39 @@ scores.append(123) // Method
286288
|Round|Round To Integer|
287289
|Sin|Sine From Degrees|
288290
|Sinr|Sine From Radians|
289-
|Torbjorn|Torbjörn|
291+
|Torbjorn|Torbjörn|
292+
293+
## Imports
294+
OWScript allows bigger scripts and scripts that use common funcitonality to be broken up into modules and imported into a base file. All the "imported" files are directly copied into the base script to be transpiled to workshop code.
295+
296+
To use this experimental feature, add the `--enable-imports` flag to the command and it will search your script for imports. This is recursive and allows for importing modules within modules. The modules you're importing **must** have the `.owpy` file ending to be properly imported or it will not be able to find the the module.
297+
298+
You can import a file by using the `#import 'filename'`
299+
300+
### Example
301+
File: `lib/functions.owpy`
302+
```
303+
%CreateEffect(pos, type, color)
304+
Create Effect
305+
Visible_To: Everyone
306+
Type: type
307+
Color: color
308+
Position: pos
309+
Radius: 1.5
310+
Reevaluation: Visible To
311+
```
312+
313+
File `src/setup.owpy`
314+
```
315+
Rule "Setup Effects"
316+
Event
317+
On Global
318+
Actions
319+
CreateEffect(<0,0,0>, Ring, Red)
320+
```
321+
322+
File: `src/game.owpy`
323+
```
324+
#import 'lib/functions'
325+
#import 'src/setup'
326+
```

Syntax/OWScript.sublime-syntax

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ contexts:
9191
- match: '(@)'
9292
captures:
9393
0: keyword.operator.OWScript
94-
- match: '(?i)(\s*\b(Event|Conditions|Actions|and|or|not|in|if|elif|else|for|while)\b)'
94+
- match: '(?i)(\s*\b(Event|Conditions|Actions|and|or|not|in|if|elif|else|for|while|#import)\b)'
9595
captures:
9696
0: keyword.OWScript
9797
- match: '(\s*([_a-zA-Z][_a-zA-Z0-9]*))'

0 commit comments

Comments
 (0)