Skip to content

Commit 3866f76

Browse files
committed
Externalize Visitor/Transformer from ast to visitor (#141), add docs
`fluent.syntax.visitor` is matching what `@fluent/syntax` does. The documentation covers more than just Visitor/Transformer, but now is as good a time as any to add that.
1 parent 893e5f4 commit 3866f76

File tree

9 files changed

+221
-66
lines changed

9 files changed

+221
-66
lines changed

fluent.syntax/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Documentation is now on https://projectfluent.org/python-fluent/fluent.syntax/.
66
- Removal of deprecated `BaseNode.traverse` method.
7+
- Refactor `Visitor` and `Transformer` into `fluent.syntax.visitor` (from `.ast`)
78

89
## fluent.syntax 0.17 (September 10, 2019)
910

fluent.syntax/docs/ast.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
AST & Tooling
2-
=============
1+
AST
2+
===
33

44

55
.. automodule:: fluent.syntax.ast

fluent.syntax/docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ post-processing of Fluent source files.
1212
:maxdepth: 2
1313
:caption: Contents:
1414

15+
usage
1516
reference
1617

1718

fluent.syntax/docs/reference.rst

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ provide more fine-grained control and detail.
1717

1818
parsing
1919
ast
20+
visitor
2021
serializing

fluent.syntax/docs/usage.rst

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
Using fluent.syntax
2+
===================
3+
4+
The ``fluent.syntax`` package provides a parser, a serializer, and libraries
5+
for analysis and processing of Fluent files.
6+
7+
Parsing
8+
-------
9+
10+
To parse a full resource, you can use the :py:func:`fluent.syntax.parse`
11+
shorthand:
12+
13+
.. code-block:: python
14+
15+
from fluent.syntax import parse
16+
resource = parse("""
17+
### Fluent resource comment
18+
19+
first = creating a { $thing }
20+
second = more content
21+
""")
22+
23+
To parse a single :py:class:`fluent.syntax.ast.Message` or :py:class:`fluent.syntax.ast.Term`, use
24+
:py:meth:`fluent.syntax.parser.FluentParser.parse_entry`:
25+
26+
.. code-block:: python
27+
28+
from fluent.syntax.parser import FluentParser
29+
parser = FluentParser()
30+
key_message = parser.parse_entry("""
31+
### Fluent resource comment
32+
33+
key = value
34+
""")
35+
36+
Serialization
37+
-------------
38+
39+
To create Fluent syntax from AST objects, use :py:func:`fluent.syntax.serialize` or
40+
:py:class:`fluent.syntax.serializer.FluentSerializer`.
41+
42+
.. code-block:: python
43+
44+
from fluent.syntax import serialize
45+
from fluent.syntax.serializer import FluentSerializer
46+
serialize(resource)
47+
serializer = FluentSerializer()
48+
serializer.serialize(resource)
49+
serializer.serialize_entry(key_message)
50+
51+
Analysis (Visitor)
52+
------------------
53+
54+
To analyze an AST tree in a read-only fashion, you can subclass
55+
:py:class:`fluent.syntax.visitor.Visitor`.
56+
57+
You overload individual :py:func:`visit_NodeName` methods to
58+
handle nodes of that type, and then call into :py:func`self.generic_visit`
59+
to continue iteration.
60+
61+
.. code-block:: python
62+
63+
from fluent.syntax import visitor
64+
import re
65+
66+
class WordCounter(visitor.Visitor):
67+
COUNTER = re.compile(r"[\w,.-]+")
68+
@classmethod
69+
def count(cls, node):
70+
wordcounter = cls()
71+
wordcounter.visit(node)
72+
return wordcounter.word_count
73+
def __init__(self):
74+
super()
75+
self.word_count = 0
76+
def visit_TextElement(self, node):
77+
self.word_count += len(self.COUNTER.findall(node.value))
78+
self.generic_visit(node)
79+
80+
WordCounter.count(resource)
81+
WordCounter.count(key_message)
82+
83+
In-place Modification (Transformer)
84+
-----------------------------------
85+
86+
Manipulation of an AST tree can be done in-place with a subclass of
87+
:py:class:`fluent.syntax.visitor.Transformer`. The coding pattern matches that
88+
of :py:class:`visitor.Visitor`, but you can modify the node in-place.
89+
You can even return different types, or remove nodes alltogether.
90+
91+
.. code-block:: python
92+
93+
class Skeleton(visitor.Transformer):
94+
def visit_SelectExpression(self, node):
95+
# This should do more checks, good enough for docs
96+
for variant in node.variants:
97+
if variant.default:
98+
default_variant = variant
99+
break
100+
template_variant = self.visit(default_variant)
101+
template_variant.default = False
102+
node.variants[:] = []
103+
for key in ('one', 'few', 'many'):
104+
variant = template_variant.clone()
105+
variant.key.name = key
106+
node.variants.append(variant)
107+
node.variants[-1].default = True
108+
return node
109+
def visit_TextElement(self, node):
110+
return None
111+
112+
skeleton = Skeleton()
113+
skeleton.visit(key_message)
114+
WordCounter.count(key_message)
115+
# Returns 0.
116+
new_plural = skeleton.visit(parser.parse_entry("""
117+
with-plural = { $num ->
118+
[one] Using { -one-term-reference } to hide
119+
*[other] Using { $num } {-term-reference} as template
120+
}
121+
"""))
122+
print(serializer.serialize_entry(new_plural))
123+
124+
This returns
125+
126+
.. code-block:: fluent
127+
128+
with-plural =
129+
{ $num ->
130+
[one] { $num }{ -term-reference }
131+
[few] { $num }{ -term-reference }
132+
*[many] { $num }{ -term-reference }
133+
}
134+
135+
136+
.. warning::
137+
138+
Serializing an AST tree that was created like this might not produce
139+
valid Fluent content.
140+

fluent.syntax/docs/visitor.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Visitor & Transformer
2+
=====================
3+
4+
5+
.. automodule:: fluent.syntax.visitor
6+
:members:
7+
:show-inheritance:

fluent.syntax/fluent/syntax/ast.py

-61
Original file line numberDiff line numberDiff line change
@@ -6,67 +6,6 @@
66
import six
77

88

9-
class Visitor(object):
10-
'''Read-only visitor pattern.
11-
12-
Subclass this to gather information from an AST.
13-
To generally define which nodes not to descend in to, overload
14-
`generic_visit`.
15-
To handle specific node types, add methods like `visit_Pattern`.
16-
If you want to still descend into the children of the node, call
17-
`generic_visit` of the superclass.
18-
'''
19-
def visit(self, node):
20-
if isinstance(node, list):
21-
for child in node:
22-
self.visit(child)
23-
return
24-
if not isinstance(node, BaseNode):
25-
return
26-
nodename = type(node).__name__
27-
visit = getattr(self, 'visit_{}'.format(nodename), self.generic_visit)
28-
visit(node)
29-
30-
def generic_visit(self, node):
31-
for propname, propvalue in vars(node).items():
32-
self.visit(propvalue)
33-
34-
35-
class Transformer(Visitor):
36-
'''In-place AST Transformer pattern.
37-
38-
Subclass this to create an in-place modified variant
39-
of the given AST.
40-
If you need to keep the original AST around, pass
41-
a `node.clone()` to the transformer.
42-
'''
43-
def visit(self, node):
44-
if not isinstance(node, BaseNode):
45-
return node
46-
47-
nodename = type(node).__name__
48-
visit = getattr(self, 'visit_{}'.format(nodename), self.generic_visit)
49-
return visit(node)
50-
51-
def generic_visit(self, node):
52-
for propname, propvalue in vars(node).items():
53-
if isinstance(propvalue, list):
54-
new_vals = []
55-
for child in propvalue:
56-
new_val = self.visit(child)
57-
if new_val is not None:
58-
new_vals.append(new_val)
59-
# in-place manipulation
60-
propvalue[:] = new_vals
61-
elif isinstance(propvalue, BaseNode):
62-
new_val = self.visit(propvalue)
63-
if new_val is None:
64-
delattr(node, propname)
65-
else:
66-
setattr(node, propname, new_val)
67-
return node
68-
69-
709
def to_json(value, fn=None):
7110
if isinstance(value, BaseNode):
7211
return value.to_json(fn)
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# coding=utf-8
2+
from __future__ import unicode_literals, absolute_import
3+
4+
from .ast import BaseNode
5+
6+
7+
class Visitor(object):
8+
'''Read-only visitor pattern.
9+
10+
Subclass this to gather information from an AST.
11+
To generally define which nodes not to descend in to, overload
12+
`generic_visit`.
13+
To handle specific node types, add methods like `visit_Pattern`.
14+
If you want to still descend into the children of the node, call
15+
`generic_visit` of the superclass.
16+
'''
17+
def visit(self, node):
18+
if isinstance(node, list):
19+
for child in node:
20+
self.visit(child)
21+
return
22+
if not isinstance(node, BaseNode):
23+
return
24+
nodename = type(node).__name__
25+
visit = getattr(self, 'visit_{}'.format(nodename), self.generic_visit)
26+
visit(node)
27+
28+
def generic_visit(self, node):
29+
for propname, propvalue in vars(node).items():
30+
self.visit(propvalue)
31+
32+
33+
class Transformer(Visitor):
34+
'''In-place AST Transformer pattern.
35+
36+
Subclass this to create an in-place modified variant
37+
of the given AST.
38+
If you need to keep the original AST around, pass
39+
a `node.clone()` to the transformer.
40+
'''
41+
def visit(self, node):
42+
if not isinstance(node, BaseNode):
43+
return node
44+
45+
nodename = type(node).__name__
46+
visit = getattr(self, 'visit_{}'.format(nodename), self.generic_visit)
47+
return visit(node)
48+
49+
def generic_visit(self, node):
50+
for propname, propvalue in vars(node).items():
51+
if isinstance(propvalue, list):
52+
new_vals = []
53+
for child in propvalue:
54+
new_val = self.visit(child)
55+
if new_val is not None:
56+
new_vals.append(new_val)
57+
# in-place manipulation
58+
propvalue[:] = new_vals
59+
elif isinstance(propvalue, BaseNode):
60+
new_val = self.visit(propvalue)
61+
if new_val is None:
62+
delattr(node, propname)
63+
else:
64+
setattr(node, propname, new_val)
65+
return node

fluent.syntax/tests/syntax/test_visitor.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
from fluent.syntax.parser import FluentParser
77
from fluent.syntax import ast
8+
from fluent.syntax import visitor
89
from tests.syntax import dedent_ftl
910

1011

11-
class MockVisitor(ast.Visitor):
12+
class MockVisitor(visitor.Visitor):
1213
def __init__(self):
1314
self.calls = defaultdict(int)
1415
self.pattern_calls = 0
@@ -80,7 +81,7 @@ def __call__(self, node):
8081
return node
8182

8283

83-
class VisitorCounter(ast.Visitor):
84+
class VisitorCounter(visitor.Visitor):
8485
def __init__(self):
8586
self.word_count = 0
8687

@@ -104,7 +105,7 @@ def __call__(self, node):
104105
return node
105106

106107

107-
class ReplaceTransformer(ast.Transformer):
108+
class ReplaceTransformer(visitor.Transformer):
108109
def __init__(self, before, after):
109110
self.before = before
110111
self.after = after

0 commit comments

Comments
 (0)