Skip to content

Commit e982a48

Browse files
committed
added PonyORM, fixed several bugs
1 parent b15edf8 commit e982a48

File tree

9 files changed

+267
-33
lines changed

9 files changed

+267
-33
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[flake8]
2-
exclude = .github,.git,__pycache__,docs/source/conf.py,old,build,dist
2+
exclude = .github,.git,__pycache__,docs/source/conf.py,old,build,dist,test.py
33
max-complexity = 10
44
max-line-length = 120

CHANGELOG.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
**v0.4.0**
2+
1. return tuples (multiple values) is parsed correctly now
3+
2. symbols like `*&^%$#!±~`§<>` now does not cause any errors
4+
3. classes without any args does not cause an error anymore
5+
16
**v0.3.0**
27
1. Added cli - `pmp` command with args -d, --dump
38
2. Added support for simple Django ORM models

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Py-Models-Parser can parse & extract information from models:
1313
- Django ORM Model,
1414
- Pydantic,
1515
- Python Enum,
16+
- Pony ORM,
1617
- Python Dataclasses
1718
- pure Python Classes
1819

@@ -176,6 +177,11 @@ For model from point 1 (above) library will produce the result:
176177
4. Add support for Piccolo ORM models
177178

178179
## Changelog
180+
**v0.4.0**
181+
1. return tuples (multiple values) is parsed correctly now
182+
2. symbols like `*&^%$#!±~`§<>` now does not cause any errors
183+
3. classes without any args does not cause an error anymore
184+
179185
**v0.3.0**
180186
1. Added cli - `pmp` command with args -d, --dump
181187
2. Added support for simple Django ORM models

docs/README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Py-Models-Parser can parse & extract information from models:
3131
* Django ORM Model,
3232
* Pydantic,
3333
* Python Enum,
34+
* Pony ORM,
3435
* Python Dataclasses
3536
* pure Python Classes
3637

@@ -200,6 +201,13 @@ TODO: in next Release
200201
Changelog
201202
---------
202203

204+
**v0.4.0**
205+
206+
207+
#. return tuples (multiple values) is parsed correctly now
208+
#. symbols like ``*&^%$#!±~``\ §<>` now does not cause any errors
209+
#. classes without any args does not cause an error anymore
210+
203211
**v0.3.0**
204212

205213

py_models_parser/grammar.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
grammar = Grammar(
44
r"""
5-
expr = (class / call_result / attr_def / emptyline / funct_def)*
6-
class = class_def attr_def* funct_def* ws?
5+
expr = (class / if_else/ call_result / return / attr_def / emptyline / funct_def)*
6+
return = "return" (id* ","*)*
7+
if_else= ("if" (compare/ id / attr_def) ":")/("elif" (id/attr_def) ":")/("else" ":")
8+
compare = (call_result / id / args /args_in_brackets ) operator (call_result/id/args_in_brackets/args)
9+
operator = "==" / "!=" / ">" / "<" / ">=" / "<="
10+
class = class_def attr_def* funct_def*
711
class_def = intend? class_name args? ":"* ws?
812
attr_def = intend? id type? ("=" (right_part))* ws?
913
right_part = (id args_in_brackets) / string / args / call_result / args_in_brackets / id / text
@@ -13,14 +17,14 @@ class = class_def attr_def* funct_def* ws?
1317
double_quotes_str = ~'"[^\"]+"'i
1418
funct_def = intend? "def" id args? ":"* ws?
1519
args_in_brackets = "[" ((id/string)* ","* )* "]"
16-
args = "(" (( call_result / args / attr_def / id )* ","* )* ")"
20+
args = "(" (( call_result / args / attr_def / id )* ","* )* ")"
1721
call_result = id args ws?
1822
class_name = "class" id
1923
id = (((dot_id / text)+ ) * / dot_id / text) ws?
2024
dot_id = (text".")*text
2125
intend = " " / "\t" / "\n"
22-
text = !class ~"['\_A-Z 0-9\{\}_\"\-\/\$<%>\+\-\w]*"i
23-
ws = ~"\s*"
26+
text = !"class" ~"['_A-Z 0-9{}_\"\-\/\$<%>\+\-\w*&^%$#!±~`§]*"i
27+
ws = ~"\\s*"
2428
emptyline = ws+
2529
"""
2630
)

py_models_parser/visitor.py

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def extract_orm_attr(self, text: str):
4848
not_orm = True
4949
properties = {}
5050
orm_columns = ["Column", "Field", "relationship", "ForeignKey"]
51+
pony_orm_fields = ["Required", "Set", "Optional", "PrimaryKey"]
52+
orm_columns.extend(pony_orm_fields)
5153
for i in orm_columns:
5254
if i in text:
5355
not_orm = False
@@ -57,34 +59,44 @@ def extract_orm_attr(self, text: str):
5759
text = text[index + 1 : -1] # noqa E203
5860
text = text.split(",")
5961
text = self.clean_up_cases_with_inner_pars(text)
62+
prop_index = 1
6063
if i == "Field":
6164
_type, properties = get_django_info(text, base_text, properties)
6265
prop_index = 0
6366
elif i == "ForeignKey":
6467
# mean it is a Django model.ForeignKey
6568
_type = "serial"
6669
properties["foreign_key"] = text[0]
67-
prop_index = 1
70+
elif i in pony_orm_fields:
71+
# mean it is a Pony ORM
72+
_type, properties = get_pony_orm_info(
73+
text, i, base_text, properties
74+
)
6875
else:
69-
prop_index = 1
7076
_type = text[0]
7177
if i == "relationship":
7278
properties["relationship"] = True
73-
for i in text[prop_index:]:
74-
if "=" in i:
75-
# can be backref=db.backref('pages', lazy=True)
76-
index = i.find("=")
77-
left = i[:index].strip()
78-
right = i[index + 1 :].strip() # noqa: E203
79-
if left == "default":
80-
default = right
81-
else:
82-
properties[left] = right
83-
elif "foreign" in i.lower():
84-
properties["foreign_key"] = i.split("(")[1].split(")")[0]
79+
for item in text[prop_index:]:
80+
properties, default = self.add_property(item, properties)
8581
break
8682
return default, _type, properties, not_orm
8783

84+
@staticmethod
85+
def add_property(item: str, properties: Dict) -> Tuple[Dict, str]:
86+
default = None
87+
if "=" in item:
88+
# can be backref=db.backref('pages', lazy=True)
89+
index = item.find("=")
90+
left = item[:index].strip()
91+
right = item[index + 1 :].strip() # noqa: E203
92+
if left == "default":
93+
default = right
94+
else:
95+
properties[left] = right
96+
elif "foreign" in item.lower():
97+
properties["foreign_key"] = item.split("(")[1].split(")")[0]
98+
return properties, default
99+
88100
def extractor(self, text: str) -> Dict:
89101
_type = None
90102
default = None
@@ -102,12 +114,11 @@ def visit_right_part(self, node, visited_children):
102114

103115
def visit_attr_def(self, node, visited_children):
104116
"""Makes a dict of the section (as key) and the key/value pairs."""
105-
left = node.children[1].children[0].text.strip()
117+
left = node.children[1].children[0].children[0].text.strip()
106118
default = None
107119
_type = None
108120
if "def " in left:
109121
attr = {"attr": {"name": None, "type": _type, "default": default}}
110-
111122
return attr
112123
if ":" in left:
113124
_type = left.split(":")[-1].strip()
@@ -183,7 +194,6 @@ def visit_expr(self, node, visited_children):
183194
children_values[n]["properties"]["init"] = final_child[
184195
"properties"
185196
]["init"]
186-
187197
return children_values
188198

189199
def visit_type(self, node, visited_children):
@@ -197,17 +207,36 @@ def generic_visit(self, node, visited_children):
197207

198208

199209
def process_no_name_attrs(final_child: Dict, child: Dict) -> None:
200-
if child["attr"]["default"]:
201-
final_child["attrs"][-1]["default"] = child["attr"]["default"]
202-
if not final_child["attrs"][-1].get("properties"):
203-
final_child["attrs"][-1]["properties"] = {}
204-
elif child["attr"]["type"]:
205-
final_child["attrs"][-1]["default"] += f':{child["attr"]["type"]}'
210+
if final_child["attrs"]:
211+
if child["attr"]["default"]:
212+
final_child["attrs"][-1]["default"] = child["attr"]["default"]
213+
if not final_child["attrs"][-1].get("properties"):
214+
final_child["attrs"][-1]["properties"] = {}
215+
elif child["attr"]["type"] and final_child["attrs"][-1]["default"]:
216+
final_child["attrs"][-1]["default"] += f':{child["attr"]["type"]}'
206217
return final_child
207218

208219

220+
def get_pony_orm_info(
221+
text: list, field: str, base_text: str, properties: Dict
222+
) -> Tuple:
223+
if field == "Required":
224+
properties["nullable"] = False
225+
elif field == "PrimaryKey":
226+
properties["primary_key"] = True
227+
elif field == "Optional":
228+
properties["nullable"] = True
229+
elif field == "Set":
230+
# relationship
231+
properties["relationship"] = True
232+
properties["foreign_key"] = text[0]
233+
_type = text[0]
234+
235+
return _type, properties
236+
237+
209238
def get_django_info(text: list, base_text: str, properties: Dict) -> Tuple:
210-
# for tortoise orm & django orm
239+
# for tortoise orm & django orm
211240
split_by_field = base_text.split("Field")[0].split(".")
212241
if len(split_by_field) == 2:
213242
_type = split_by_field[1]

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "py-models-parser"
3-
version = "0.3.0"
4-
description = "Parser for Different Python Models (Pydantic, Enums, ORMs: Tortoise, SqlAlchemy, GinoORM) to extract information about columns(attrs), model, table args,etc in one format."
3+
version = "0.4.0"
4+
description = "Parser for Different Python Models (Pydantic, Enums, ORMs: Tortoise, SqlAlchemy, GinoORM, PonyORM) to extract information about columns(attrs), model, table args,etc in one format."
55
authors = ["Iuliia Volkova <[email protected]>"]
66
license = "MIT"
77
readme = "docs/README.rst"

tests/test_pony.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from py_models_parser import parse
2+
3+
4+
def test_simple_pony_model():
5+
model = """
6+
class Person(db.Entity):
7+
name = Required(str)
8+
age = Required(int)
9+
cars = Set('Car')
10+
11+
class Car(db.Entity):
12+
make = Required(str)
13+
model = Required(str)
14+
owner = Required(Person)
15+
16+
"""
17+
18+
result = parse(model)
19+
expected = [
20+
{
21+
"attrs": [
22+
{
23+
"default": None,
24+
"name": "name",
25+
"properties": {"nullable": False},
26+
"type": "str",
27+
},
28+
{
29+
"default": None,
30+
"name": "age",
31+
"properties": {"nullable": False},
32+
"type": "int",
33+
},
34+
{
35+
"default": None,
36+
"name": "cars",
37+
"properties": {"foreign_key": "'Car'", "relationship": True},
38+
"type": "'Car'",
39+
},
40+
],
41+
"name": "Person",
42+
"parents": ["db.Entity"],
43+
"properties": {},
44+
},
45+
{
46+
"attrs": [
47+
{
48+
"default": None,
49+
"name": "make",
50+
"properties": {"nullable": False},
51+
"type": "str",
52+
},
53+
{
54+
"default": None,
55+
"name": "model",
56+
"properties": {"nullable": False},
57+
"type": "str",
58+
},
59+
{
60+
"default": None,
61+
"name": "owner",
62+
"properties": {"nullable": False},
63+
"type": "Person",
64+
},
65+
],
66+
"name": "Car",
67+
"parents": ["db.Entity"],
68+
"properties": {},
69+
},
70+
]
71+
assert result == expected
72+
73+
74+
def test_primary_and_optional():
75+
76+
model = """
77+
from pony.orm import *
78+
79+
db = Database()
80+
81+
82+
class Product(db.Entity):
83+
id = PrimaryKey(int, auto=True)
84+
name = Required(str)
85+
info = Required(Json)
86+
tags = Optional(Json)
87+
88+
89+
db.bind('sqlite', ':memory:', create_db=True)
90+
db.generate_mapping(create_tables=True)
91+
"""
92+
93+
result = parse(model)
94+
expected = [
95+
{
96+
"attrs": [
97+
{
98+
"default": None,
99+
"name": "id",
100+
"properties": {"auto": "True", "primary_key": True},
101+
"type": "int",
102+
},
103+
{
104+
"default": None,
105+
"name": "name",
106+
"properties": {"nullable": False},
107+
"type": "str",
108+
},
109+
{
110+
"default": None,
111+
"name": "info",
112+
"properties": {"nullable": False},
113+
"type": "Json",
114+
},
115+
{
116+
"default": None,
117+
"name": "tags",
118+
"properties": {"nullable": True},
119+
"type": "Json",
120+
},
121+
{"default": None, "name": "db.bind", "type": None},
122+
],
123+
"name": "Product",
124+
"parents": ["db.Entity"],
125+
"properties": {},
126+
}
127+
]
128+
assert result == expected

0 commit comments

Comments
 (0)