Skip to content

Commit b15edf8

Browse files
committed
added support for Django ORM Models & Python Classes
1 parent dd02598 commit b15edf8

File tree

7 files changed

+488
-34
lines changed

7 files changed

+488
-34
lines changed

CHANGELOG.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
**v0.3.0**
22
1. Added cli - `pmp` command with args -d, --dump
3-
2. Add support for pure Python classes
3+
2. Added support for simple Django ORM models
4+
3. Added base support for pure Python Classes
45

56
**v0.2.0**
67
1. Added support for Dataclasses

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ Py-Models-Parser can parse & extract information from models:
1010
- Sqlalchemy ORM,
1111
- Gino ORM,
1212
- Tortoise ORM,
13+
- Django ORM Model,
1314
- Pydantic,
1415
- Python Enum,
1516
- Python Dataclasses
17+
- pure Python Classes
1618

1719
Number of supported models will be increased, check 'TODO' section.
1820

@@ -168,15 +170,21 @@ For model from point 1 (above) library will produce the result:
168170

169171
## TODO: in next Release
170172

171-
1. Add more tests for supported models (and fix existed not covered cases): Pydantic, Enums, Dataclasses, SQLAlchemy Models, GinoORM models, TortoiseORM models
172-
2. Add support for pure SQLAlchemy Core Tables
173+
1. Add more tests for supported models (and fix existed not covered cases): Django ORM, Pydantic, Enums, Dataclasses, SQLAlchemy Models, GinoORM models, TortoiseORM models
174+
2. Add support for SQLAlchemy Core Tables
173175
3. Add support for Pony ORM models
176+
4. Add support for Piccolo ORM models
174177

175178
## Changelog
179+
**v0.3.0**
180+
1. Added cli - `pmp` command with args -d, --dump
181+
2. Added support for simple Django ORM models
182+
3. Added base support for pure Python Classes
183+
176184
**v0.2.0**
177185
1. Added support for Dataclasses
178186
2. Added parse_from_file method
179-
3. Added correct work with types with comma inside, like: Union[dict, list] or Union[dict, list, tuple, anything]
187+
3. Added correct work with types with comma inside, like: Union[dict, list] or Union[dict, list, tuple, anything]
180188

181189
**v0.1.1**
182190
1. Added base parser logic & tests for Pydantic, Enums, SQLAlchemy Models, GinoORM models, TortoiseORM models

docs/README.rst

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@ Py-Models-Parser
2121

2222

2323
It's as second Parser that done by me, first is a https://github.com/xnuinside/simple-ddl-parser for SQL DDL with different dialects.
24-
Py-Models-Parser supports now ORM Sqlalchemy, Gino, Tortoise; Pydantic, Python Enum models, Dataclasses & in nearest feature I plan to add pure pyton classes. And next will be added other ORMs models.
24+
25+
Py-Models-Parser can parse & extract information from models:
26+
27+
28+
* Sqlalchemy ORM,
29+
* Gino ORM,
30+
* Tortoise ORM,
31+
* Django ORM Model,
32+
* Pydantic,
33+
* Python Enum,
34+
* Python Dataclasses
35+
* pure Python Classes
36+
37+
Number of supported models will be increased, check 'TODO' section.
2538

2639
Py-Models-Parser written with PEG parser and it's python implementation - parsimonious. It's pretty new and I did not cover all possible test cases, so if you will have an issue - please just open an issue in this case with example, I will fix it as soon as possible.
2740

@@ -69,7 +82,8 @@ How to use
6982

7083
Library detect automaticaly that type of models you tries to parse. You can check a lot of examples in test/ folder on the GitHub
7184

72-
You can parse models from python string:
85+
86+
#. You can parse models from python string:
7387

7488
.. code-block:: python
7589
@@ -92,7 +106,8 @@ You can parse models from python string:
92106
"""
93107
result = parse(models_str)
94108
95-
or just provide the path to file:
109+
110+
#. Parse models from file:
96111

97112
.. code-block:: python
98113
@@ -104,7 +119,31 @@ or just provide the path to file:
104119
# for example: tests/data/dataclass_defaults.py
105120
result = parse_from_file(file_path)
106121
107-
It will produce the result:
122+
123+
#. Parse models from file with command line
124+
125+
.. code-block:: bash
126+
127+
128+
pmp path_to_models.py
129+
130+
# for example: pmp tests/data/dataclass_defaults.py
131+
132+
Output from cli can be dumped in 'output_models.json' file - use flag '-d' '--dump' if you want to change target file name, provide it after argument like '-d target_file.json'
133+
134+
.. code-block:: bash
135+
136+
137+
# example how to dump output from cli
138+
139+
pmp path_to_models.py -d target_file.json
140+
141+
Output example
142+
^^^^^^^^^^^^^^
143+
144+
You can find a lot of output examples in tests - https://github.com/xnuinside/py-models-parser/tree/main/tests
145+
146+
For model from point 1 (above) library will produce the result:
108147

109148
.. code-block:: python
110149
@@ -153,15 +192,21 @@ TODO: in next Release
153192
---------------------
154193

155194

156-
#. Parse from file method
157-
#. Add cli
158-
#. Add more tests for supported models (and fix existed not covered cases): Pydantic, Enums, Dataclasses, SQLAlchemy Models, GinoORM models, TortoiseORM models
159-
#. Add support for pure Python classes
160-
#. Add support for pure SQLAlchemy Core Tables
195+
#. Add more tests for supported models (and fix existed not covered cases): Django ORM, Pydantic, Enums, Dataclasses, SQLAlchemy Models, GinoORM models, TortoiseORM models
196+
#. Add support for SQLAlchemy Core Tables
197+
#. Add support for Pony ORM models
198+
#. Add support for Piccolo ORM models
161199

162200
Changelog
163201
---------
164202

203+
**v0.3.0**
204+
205+
206+
#. Added cli - ``pmp`` command with args -d, --dump
207+
#. Added support for simple Django ORM models
208+
#. Added base support for pure Python Classes
209+
165210
**v0.2.0**
166211

167212

py_models_parser/grammar.py

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

33
grammar = Grammar(
44
r"""
5-
expr = (class / call_result / attr_def / emptyline)*
6-
class = class_def attr_def* ws?
5+
expr = (class / call_result / attr_def / emptyline / funct_def)*
6+
class = class_def attr_def* funct_def* ws?
77
class_def = intend? class_name args? ":"* ws?
8-
attr_def = intend? id ("=" right_part)* ws?
9-
right_part = args / call_result / id / string / text
8+
attr_def = intend? id type? ("=" (right_part))* ws?
9+
right_part = (id args_in_brackets) / string / args / call_result / args_in_brackets / id / text
10+
type = ":" ( (id args_in_brackets) / id)
1011
string = one_quote_str / double_quotes_str
11-
one_quote_str = ~"\'[^\']+\'"
12-
double_quotes_str = ~'"[^\"]+"'
12+
one_quote_str = ~"\'[^\']+\'"i
13+
double_quotes_str = ~'"[^\"]+"'i
14+
funct_def = intend? "def" id args? ":"* ws?
15+
args_in_brackets = "[" ((id/string)* ","* )* "]"
1316
args = "(" (( call_result / args / attr_def / id )* ","* )* ")"
1417
call_result = id args ws?
1518
class_name = "class" id
16-
id = (((dot_id / text)+ ","*) * / dot_id / text) ws?
19+
id = (((dot_id / text)+ ) * / dot_id / text) ws?
1720
dot_id = (text".")*text
18-
intend = " " / "\t"
19-
text = !class ~"['\_A-Z 0-9\{\}\[\]_\"\-\/\$:<%>\w]*"i
21+
intend = " " / "\t" / "\n"
22+
text = !class ~"['\_A-Z 0-9\{\}_\"\-\/\$<%>\+\-\w]*"i
2023
ws = ~"\s*"
2124
emptyline = ws+
2225
"""

py_models_parser/visitor.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, Tuple
22

33
from parsimonious.nodes import NodeVisitor
44

@@ -47,7 +47,7 @@ def extract_orm_attr(self, text: str):
4747
default = None
4848
not_orm = True
4949
properties = {}
50-
orm_columns = ["Column", "Field", "relationship"]
50+
orm_columns = ["Column", "Field", "relationship", "ForeignKey"]
5151
for i in orm_columns:
5252
if i in text:
5353
not_orm = False
@@ -56,16 +56,15 @@ def extract_orm_attr(self, text: str):
5656
base_text = text
5757
text = text[index + 1 : -1] # noqa E203
5858
text = text.split(",")
59-
6059
text = self.clean_up_cases_with_inner_pars(text)
6160
if i == "Field":
62-
# for tortoise orm
63-
split_by_field = base_text.split("Field")[0].split(".")
64-
if len(split_by_field) == 2:
65-
_type = split_by_field[1]
66-
else:
67-
_type = split_by_field[0]
61+
_type, properties = get_django_info(text, base_text, properties)
6862
prop_index = 0
63+
elif i == "ForeignKey":
64+
# mean it is a Django model.ForeignKey
65+
_type = "serial"
66+
properties["foreign_key"] = text[0]
67+
prop_index = 1
6968
else:
7069
prop_index = 1
7170
_type = text[0]
@@ -108,33 +107,48 @@ def visit_attr_def(self, node, visited_children):
108107
_type = None
109108
if "def " in left:
110109
attr = {"attr": {"name": None, "type": _type, "default": default}}
110+
111111
return attr
112112
if ":" in left:
113113
_type = left.split(":")[-1].strip()
114114
left = left.split(":")[0].strip()
115115
attr = {"attr": {"name": left, "type": _type, "default": default}}
116116
for children in visited_children:
117+
117118
if isinstance(children, list):
118119
if isinstance(children[-1], list):
119120
if "default" in children[-1][-1]:
120121
attr["attr"]["default"] = children[-1][-1]["default"]
121122
attr["attr"]["properties"] = children[-1][-1]["properties"]
122123
if children[-1][-1]["type"] is not None:
123124
attr["attr"]["type"] = children[-1][-1]["type"]
125+
elif isinstance(children[-1], dict) and "type" in children[-1]:
126+
attr["attr"]["type"] = children[-1]["type"]
124127
return attr
125128

126129
def process_chld(self, child, final_child):
127130
if "attr" in child and child["attr"]["name"]:
128-
if "tablename" in child["attr"]["name"]:
131+
# todo: this is a hack, need refactor it
132+
if child["attr"]["name"] == "self" and not final_child["properties"].get(
133+
"init"
134+
):
135+
final_child["properties"]["init"] = []
136+
elif "tablename" in child["attr"]["name"]:
129137
final_child["properties"]["table_name"] = child["attr"]["default"]
130138
elif "table_args" in child["attr"]["name"]:
131139
final_child["properties"][child["attr"]["name"]] = (
132140
child["attr"]["type"] or child["attr"]["default"]
133141
)
134142
else:
135-
final_child["attrs"].append(child["attr"])
143+
if final_child["properties"].get("init") is not None:
144+
final_child["properties"]["init"].append(child["attr"])
145+
else:
146+
final_child["attrs"].append(child["attr"])
136147
else:
137-
if isinstance(child, dict):
148+
149+
if "attr" in child:
150+
final_child = process_no_name_attrs(final_child, child)
151+
elif isinstance(child, dict):
138152
final_child.update(child)
139153
elif isinstance(child, list):
140154
for i in child:
@@ -162,8 +176,43 @@ def visit_expr(self, node, visited_children):
162176
n += 1
163177
if "attr" in final_child:
164178
del final_child["attr"]
179+
if final_child["properties"].get("init") == []:
180+
del final_child["properties"]["init"]
181+
elif final_child["properties"].get("init"):
182+
if not children_values[n]["properties"].get("init"):
183+
children_values[n]["properties"]["init"] = final_child[
184+
"properties"
185+
]["init"]
186+
165187
return children_values
166188

189+
def visit_type(self, node, visited_children):
190+
_index = node.text.find(":")
191+
_type = node.text[_index + 1 :] # noqa: E203
192+
return {"type": _type.strip()}
193+
167194
def generic_visit(self, node, visited_children):
168195
"""The generic visit method."""
169196
return visited_children or node
197+
198+
199+
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"]}'
206+
return final_child
207+
208+
209+
def get_django_info(text: list, base_text: str, properties: Dict) -> Tuple:
210+
# for tortoise orm & django orm
211+
split_by_field = base_text.split("Field")[0].split(".")
212+
if len(split_by_field) == 2:
213+
_type = split_by_field[1]
214+
else:
215+
_type = split_by_field[0]
216+
if _type == "ManyToMany":
217+
properties["foreign_key"] = text[0]
218+
return _type, properties

0 commit comments

Comments
 (0)