Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

220 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

CIRelease VersionGithub stargazers

Python Left-Right Parser

Pyleri is an easy-to-use parser created for SiriDB. We first used lrparsing and wrote jsleri for auto-completion and suggestions in our web console. Later we found small issues within the lrparsing module and also had difficulties keeping the language the same in all projects. That is when we decided to create Pyleri which can export a created grammar to JavaScript, C, Python, Go and Java.

Gabriele Tomassetti wrote a tutorial about the pyleri library.



Related projects

Installation

The easiest way is to use PyPI:

sudo pip3 install pyleri

Quick usage

# Imports, note that we skip the imports in other examples...frompyleriimport (
Grammar,
Keyword,
Regex,
Sequence)
# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, r_name)
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
# Use the compiled grammar to parse 'strings'print(my_grammar.parse('hi "Iris"').is_valid) # => Trueprint(my_grammar.parse('bye "Iris"').is_valid) # => Falseprint(my_grammar.parse('bye "Iris"').as_str()) # => error at position 0, expecting: hi

Grammar

When writing a grammar you should subclass Grammar. A Grammar expects at least a START property so the parser knows where to start parsing. Grammar has some default properties which can be overwritten like RE_KEYWORDS, which will be explained later. Grammar also has a parse method: parse(), and a few export methods: export_js(), export_c(), export_py(), export_go() and export_java() which are explained below.

parse

syntax:

Grammar().parse(string)

The parse() method returns a result object which has the following properties that are further explained in Result:

  • expecting
  • is_valid
  • pos
  • tree

export_js

syntax:

Grammar().export_js(
js_module_name='jsleri',
js_template=Grammar.JS_TEMPLATE,
js_indent=' '*4)

Optional keyword arguments:

  • js_module_name: Name of the JavaScript module. (default: 'jsleri')
  • js_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JS_TEMPLATE.
  • js_indent: indentation used in the JavaScript file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_js():

/* jshint newcap: false *//* * This grammar is generated using the Grammar.export_js() method and * should be used with the jsleri JavaScript module. * * Source class: MyGrammar * Created at: 2015-11-04 10:06:06 */'use strict';(function(Regex,Sequence,Keyword,Grammar){varr_name=Regex('^(?:"(?:[^"]*)")+');vark_hi=Keyword('hi');varSTART=Sequence(k_hi,r_name);window.MyGrammar=Grammar(START,'^\w+');})(window.jsleri.Regex,window.jsleri.Sequence,window.jsleri.Keyword,window.jsleri.Grammar);

export_c

syntax:

Grammar().export_c(
target=Grammar.C_TARGET,
c_indent=' '*4)

Optional keyword arguments:

  • target: Name of the c module. (default: 'grammar')
  • c_indent: indentation used in the c files. (default: 4 spaces)

The return value is a tuple containing the source (c) file and header (h) file.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_c():

/* * grammar.c * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#include"grammar.h"#include<stdio.h>#defineCLERI_CASE_SENSITIVE 0
#defineCLERI_CASE_INSENSITIVE 1
#defineCLERI_FIRST_MATCH 0
#defineCLERI_MOST_GREEDY 1
cleri_grammar_t*compile_grammar(void)
{
cleri_t*r_name=cleri_regex(CLERI_GID_R_NAME, "^(?:\"(?:[^\"]*)\")+");
cleri_t*k_hi=cleri_keyword(CLERI_GID_K_HI, "hi", CLERI_CASE_INSENSITIVE);
cleri_t*START=cleri_sequence(
CLERI_GID_START,
2,
k_hi,
r_name
);
cleri_grammar_t*grammar=cleri_grammar(START, "^\\w+");
returngrammar;
}

and the header file...

/* * grammar.h * * This grammar is generated using the Grammar.export_c() method and * should be used with the libcleri module. * * Source class: MyGrammar * Created at: 2016-05-09 12:16:49 */#ifndefCLERI_EXPORT_GRAMMAR_H_#defineCLERI_EXPORT_GRAMMAR_H_#include<grammar.h>#include<cleri/cleri.h>cleri_grammar_t*compile_grammar(void);
enumcleri_grammar_ids {
CLERI_NONE, // used for objects with no nameCLERI_GID_K_HI,
CLERI_GID_R_NAME,
CLERI_GID_START,
CLERI_END// can be used to get the enum length
};
#endif/* CLERI_EXPORT_GRAMMAR_H_ */

export_go

syntax:

Grammar().export_go(
go_template=Grammar.GO_TEMPLATE,
go_indent='\t',
go_package='grammar')

Optional keyword arguments:

  • go_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.GO_TEMPLATE.
  • go_indent: indentation used in the Go file. (default: one tab)
  • go_package: Name of the go package. (default: 'grammar')

For example when using our Quick usage grammar, this is the output when running my_grammar.export_go():

package grammar
// This grammar is generated using the Grammar.export_go() method and// should be used with the goleri module.//// Source class: MyGrammar// Created at: 2017-03-14 19:07:09import (
"regexp""github.com/cesbit/goleri"
)
// Element indentifiersconst (
NoGid=iotaGidKHi=iotaGidRName=iotaGidSTART=iota
)
// MyGrammar returns a compiled goleri grammar.funcMyGrammar() *goleri.Grammar {
rName:=goleri.NewRegex(GidRName, regexp.MustCompile(`^(?:"(?:[^"]*)")+`))
kHi:=goleri.NewKeyword(GidKHi, "hi", false)
START:=goleri.NewSequence(
GidSTART,
kHi,
rName,
)
returngoleri.NewGrammar(START, regexp.MustCompile(`^\w+`))
}

export_java

syntax:

Grammar().export_java(
java_template=Grammar.JAVA_TEMPLATE,
java_indent=' '*4,
java_package=None,
is_public=True)

Optional keyword arguments:

  • java_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.JAVA_TEMPLATE.
  • java_indent: indentation used in the Java file. (default: four spaces)
  • java_package: Name of the Java package or None when no package is specified. (default: None)
  • is_public: Class and constructor are defined as public when True, else they will be defined as package private.

For example when using our Quick usage grammar, this is the output when running my_grammar.export_java():

/** * This grammar is generated using the Grammar.export_java() method and * should be used with the jleri module. * * Source class: MyGrammar * Created at: 2018-07-04 12:12:34 */importjleri.Grammar;
importjleri.Element;
importjleri.Sequence;
importjleri.Regex;
importjleri.Keyword;
publicclassMyGrammarextendsGrammar {
enumIds {
K_HI,
R_NAME,
START
}
privatestaticfinalElementR_NAME = newRegex(Ids.R_NAME, "^(?:\"(?:[^\"]*)\")+");
privatestaticfinalElementK_HI = newKeyword(Ids.K_HI, "hi", false);
privatestaticfinalElementSTART = newSequence(
Ids.START,
K_HI,
R_NAME
);
publicMyGrammar() {
super(START, "^\\w+");
}
}

export_py

syntax:

Grammar().export_py(
py_module_name='pyleri',
py_template=Grammar.PY_TEMPLATE,
py_indent=' '*4)

Optional keyword arguments:

  • py_module_name: Name of the Pyleri Module. (default: 'pyleri')
  • py_template: Template String used for the export. You might want to look at the default string which can be found at Grammar.PY_TEMPLATE.
  • py_indent: indentation used in the Python file. (default: 4 spaces)

For example when using our Quick usage grammar, this is the output when running my_grammar.export_py():

""" This grammar is generated using the Grammar.export_py() method and should be used with the pyleri python module. Source class: MyGrammar Created at: 2017-03-14 19:14:51"""importrefrompyleriimportSequencefrompyleriimportKeywordfrompyleriimportGrammarfrompyleriimportRegexclassMyGrammar(Grammar):
RE_KEYWORDS=re.compile('^\\w+')
r_name=Regex('^(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(
k_hi,
r_name
)

Result

The result of the parse() method contains 4 properties that will be explained next. A function as_str(translate=None) is also available which will show the result as a string. The translate argument should be a function which accepts an element as argument. This function can be used to return custom strings for certain elements. If the return value of translate is None then the function will fall try to generate a string value. If the return value is an empty string, the value will be ignored.

Example of translate functions:

# In case a translation function returns an empty string, no text is useddeftranslate(elem):
return''# as a result you get something like: 'error at position x'# Text may be returned based on giddeftranslate(elem):
ifelemissome_elem:
return'A'# something like: error at position x, expecting: Aelifelemisother_elem:
return''# other_elem will be ignoredelse:
returnNone# normal parsing# A translate function can be used as follow:print(my_grammar.parse('some string').as_str(translate=translate))

is_valid

is_valid returns a boolean value, True when the given string is valid according to the given grammar, False when not valid.

Let us take the example from Quick usage.

res=my_grammar.parse('bye "Iris"')
print(res.is_valid) # => False

Position

pos returns the position where the parser had to stop. (when is_valid is True this value will be equal to the length of the given string with str.rstrip() applied)

Let us take the example from Quick usage.

result=my_grammar.parse('hi Iris')
print(res.is_valid, result.pos) # => False, 3

Tree

tree contains the parse tree. Even when is_valid is False the parse tree is returned but will only contain results as far as parsing has succeeded. The tree is the root node which can include several children nodes. The structure will be further clarified in the following example which explains a way of visualizing the parse tree.

Example:

importjsonfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRegexfrompyleriimportRepeatfrompyleriimportSequence# Create a Grammar Class to define your languageclassMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name))
# Returns properties of a node object as a dictionary:defnode_props(node, children):
return {
'start': node.start,
'end': node.end,
'name': node.element.nameifhasattr(node.element, 'name') elseNone,
'element': node.element.__class__.__name__,
'string': node.string,
'children': children}
# Recursive method to get the children of a node object:defget_children(children):
return [node_props(c, get_children(c.children)) forcinchildren]
# View the parse tree:defview_parse_tree(res):
start=res.tree.children[0] \
ifres.tree.childrenelseres.treereturnnode_props(start, get_children(start.children))
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class:my_grammar=MyGrammar()
res=my_grammar.parse('hi "pyleri" bye "pyleri"')
# The parse tree is visualized as a JSON object:print(json.dumps(view_parse_tree(res), indent=2))

Part of the output is shown below.

 {
"start": 0,
"end": 23,
"name": "START",
"element": "Repeat",
"string": "hi \"pyleri\" bye \"pyleri\"",
"children": [
{
"start": 0,
"end": 11,
"name": null,
"element": "Sequence",
"string": "hi \"pyleri\"",
"children": [
{
"start": 0,
"end": 2,
"name": null,
"element": "Choice",
"string": "hi",
"children": [
{
"start": 0,
"end": 2,
"name": "k_hi",
"element": "Keyword",
"string": "hi",
"children": []
}
]
},
{
"start": 3,
"end": 11,
"name": "r_name",
"element": "Regex",
"string": "\"pyleri\"",
"children": []
}
"...""..."

A node contains 5 properties that will be explained next:

  • start property returns the start of the node object.
  • end property returns the end of the node object.
  • element returns the Element's type (e.g. Repeat, Sequence, Keyword, etc.). An element can be assigned to a variable; for instance in the example above Keyword('hi') was assigned to k_hi. With element.name the assigned name k_hi will be returned. Note that it is not a given that an element is named; in our example Sequence was not assigned, thus in this case the element has no attribute name.
  • string returns the string that is parsed.
  • children can return a node object containing deeper layered nodes provided that there are any. In our example the root node has an element type Repeat(), starts at 0 and ends at 24, and it has two children. These children are node objects that have both an element type Sequence, start at 0 and 12 respectively, and so on.

Expecting

expecting returns a Python set() containing elements which pyleri expects at pos. Even if is_valid is true there might be elements in this set, for example when an Optional() element could be added to the string. "Expecting" is useful if you want to implement things like auto-completion, syntax error handling, auto-syntax-correction etc. The following example will illustrate a way of implementation.

Example:

importreimportrandomfrompyleriimportChoicefrompyleriimportGrammarfrompyleriimportKeywordfrompyleriimportRepeatfrompyleriimportSequencefrompyleriimportend_of_statement# Create a Grammar Class to define your language.classMyGrammar(Grammar):
RE_KEYWORDS=re.compile(r'\S+')
r_name=Keyword('"pyleri"')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Repeat(Sequence(Choice(k_hi, k_bye), r_name), mi=2)
# Print the expected elements as a indented and numbered list.defprint_expecting(node_expecting, string_expecting):
forloop, einenumerate(node_expecting):
string_expecting='{}\n\t({}) {}'.format(string_expecting, loop, e)
print(string_expecting)
# Complete a string until it is valid according to the grammar.defauto_correction(string, my_grammar):
node=my_grammar.parse(string)
print('\nParsed string: {}'.format(node.tree.string))
ifnode.is_valid:
string_expecting='String is valid. \nExpected: 'print_expecting(node.expecting, string_expecting)
else:
string_expecting='String is NOT valid.\nExpected: ' \
ifnotnode.pos \
else'String is NOT valid. \nAfter "{}" expected: '.format(
node.tree.string[:node.pos])
print_expecting(node.expecting, string_expecting)
selected=random.choice(list(node.expecting))
string='{} {}'.format(node.tree.string[:node.pos],
selectedifselectedisnotend_of_statementelse'')
auto_correction(string, my_grammar)
if__name__=='__main__':
# Compile your grammar by creating an instance of the Grammar Class.my_grammar=MyGrammar()
string='hello "pyleri"'auto_correction(string, my_grammar)

Output:

Parsed string: hello "pyleri"
String is NOT valid.
Expected:
(1) hi
(2) bye
Parsed string: bye
String is NOT valid.
After " bye" expected:
(1) "pyleri"
Parsed string: bye "pyleri"
String is NOT valid.
After " bye "pyleri"" expected:
(1) hi
(2) bye
Parsed string: bye "pyleri" hi
String is NOT valid.
After " bye "pyleri" hi" expected:
(1) "pyleri"
Parsed string: bye "pyleri" hi "pyleri"
String is valid.
Expected:
(1) hi
(2) bye

In the above example we parsed an invalid string according to the grammar class. The auto-correction() method that we built for this example combines all properties from the parse() to create a valid string. The output shows every recursion of the auto-correction() method and prints successively the set of expected elements. It takes one randomly and adds it to the string. When the string corresponds to the grammar, the property is_valid will return True. Notably the expecting property still contains elements even if the is_valid returned True. The reason in this example is due to the Repeat element.

Elements

Pyleri has several elements which are all subclasses of Element and can be used to create a grammar.

Keyword

syntax:

Keyword(keyword, ign_case=False)

The parser needs to match the keyword which is just a string. When matching keywords we need to tell the parser what characters are allowed in keywords. By default Pyleri uses ^\w+ which is both in Python and JavaScript equal to ^[A-Za-z0-9_]+. We can overwrite the default by setting RE_KEYWORDS in the grammar. Keyword() accepts one keyword argument ign_case to tell the parser if we should match case insensitive.

Example:

classTicTacToe(Grammar):
# Let's allow keywords with alphabetic characters and dashes.RE_KEYWORDS=re.compile('^[A-Za-z-]+')
START=Keyword('tic-tac-toe', ign_case=True)
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic-Tac-Toe').is_valid# => True

Regex

syntax:

Regex(pattern, flags=0)

The parser compiles a regular expression using the re module. The current version of pyleri has only support for the re.IGNORECASE flag. See the Quick usage example for how to use Regex.

Token

syntax:

Token(token)

A token can be one or more characters and is usually used to match operators like +, -, // and so on. When we parse a string object where pyleri expects an element, it will automatically be converted to a Token() object.

Example:

classNi(Grammar):
t_dash=Token('-')
# We could just write delimiter='-' because# any string will be converted to Token()START=List(Keyword('ni'), delimiter=t_dash)
ni=Ni()
ni.parse('ni-ni-ni-ni-ni').is_valid# => True

Tokens

syntax:

Tokens(tokens)

Can be used to register multiple tokens at once. The tokens argument should be a string with tokens separated by spaces. If given tokens are different in size the parser will try to match the longest tokens first.

Example:

classNi(Grammar):
tks=Tokens('+ - !=')
START=List(Keyword('ni'), delimiter=tks)
ni=Ni()
ni.parse('ni + ni != ni - ni').is_valid# => True

Sequence

syntax:

Sequence(element, element, ...)

The parser needs to match each element in a sequence.

Example:

classTicTacToe(Grammar):
START=Sequence(Keyword('Tic'), Keyword('Tac'), Keyword('Toe'))
ttt_grammar=TicTacToe()
ttt_grammar.parse('Tic Tac Toe').is_valid# => True

Choice

syntax:

Choice(element, element, ..., most_greedy=True)

The parser needs to choose between one of the given elements. Choice accepts one keyword argument most_greedy which is True by default. When most_greedy is set to False the parser will stop at the first match. When True the parser will try each element and returns the longest match. Setting most_greedy to False can provide some extra performance. Note that the parser will try to match each element in the exact same order they are parsed to Choice.

Example: let us use Choice to modify the Quick usage example to allow the string 'bye "Iris"'

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
k_bye=Keyword('bye')
START=Sequence(Choice(k_hi, k_bye), r_name)
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('bye "Iris"').is_valid# => True

Repeat

syntax:

Repeat(element, mi=0, ma=None)

The parser needs at least mi elements and at most ma elements. When ma is set to None we allow unlimited number of elements. mi can be any integer value equal or higher than 0 but not larger then ma.

Example:

classNi(Grammar):
START=Repeat(Keyword('ni'))
ni=Ni()
ni.parse('ni ni ni ni ni').is_valid# => True

It is not allowed to bind a name to the same element twice and Repeat(element, 1, 1) is a common solution to bind the element a second (or more) time(s).

For example consider the following:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
# Raises a SyntaxError because we try to bind a second time.r_address=r_name# WRONG# Instead use Repeatr_address=Repeat(r_name, 1, 1) # RIGHT

List

syntax:

List(element, delimiter=',', mi=0, ma=None, opt=False)

List is like Repeat but with a delimiter. A comma is used as default delimiter but any element is allowed. When a string is used as delimiter it will be converted to a Token element. mi and ma work exactly like with Repeat. An optional keyword argument opt can be set to True to allow the list to end with a delimiter. By default this is set to False which means the list has to end with an element.

Example:

classNi(Grammar):
START=List(Keyword('ni'))
ni=Ni()
ni.parse('ni, ni, ni, ni, ni').is_valid# => True

Optional

syntax:

Optional(element)

The parser looks for an optional element. It is like using Repeat(element, 0, 1) but we encourage to use Optional since it is more readable. (and slightly faster)

Example:

classMyGrammar(Grammar):
r_name=Regex('(?:"(?:[^"]*)")+')
k_hi=Keyword('hi')
START=Sequence(k_hi, Optional(r_name))
my_grammar=MyGrammar()
my_grammar.parse('hi "Iris"').is_valid# => Truemy_grammar.parse('hi').is_valid# => True

Ref

syntax:

Ref()

The grammar can make a forward reference to make recursion possible. In the example below we create a forward reference to START but note that a reference to any element can be made.

Warning: A reference is not protected against testing the same position in a string. This could potentially lead to an infinite loop. For example:

r=Ref()
r=Optional(r) # DON'T DO THIS

Use Prio if such recursive construction is required.

Example:

classNestedNi(Grammar):
START=Ref()
ni_item=Choice(Keyword('ni'), START)
START=Sequence('[', List(ni_item), ']')
nested_ni=NestedNi()
nested_ni.parse('[ni, ni, [ni, [], [ni, ni]]]').is_valid# => True

Prio

syntax:

Prio(element, element, ...)

Choose the first match from the prio elements and allow THIS for recursive operations. With THIS we point to the Prio element. Probably the example below explains how Prio and THIS can be used.

Note: Use a Ref when possible. A Prio element is required when the same position in a string is potentially checked more than once.

Example:

classNi(Grammar):
k_ni=Keyword('ni')
START=Prio(
k_ni,
# '(' and ')' are automatically converted to Token('(') and Token(')')Sequence('(', THIS, ')'),
Sequence(THIS, Keyword('or'), THIS),
Sequence(THIS, Keyword('and'), THIS))
ni=Ni()
ni.parse('(ni or ni) and (ni or ni)').is_valid# => True

About

Python Parser

Resources

Stars

124 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages