drf-to-mkdoc 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of drf-to-mkdoc might be problematic. Click here for more details.

@@ -1,327 +1,327 @@
1
- import inspect
2
- import json
3
- import re
4
- from pathlib import Path
5
-
6
- from django.apps import apps
7
- from django.core.management.base import BaseCommand
8
- from django.db import models
9
-
10
- from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
11
-
12
- class Command(BaseCommand):
13
- help = "Generate model documentation JSON from Django model introspection"
14
-
15
- def add_arguments(self, parser):
16
- parser.add_argument(
17
- "--output",
18
- type=str,
19
- default=drf_to_mkdoc_settings.MODEL_DOCS_FILE,
20
- help=f"Output JSON file name (default: {drf_to_mkdoc_settings.MODEL_DOCS_FILE})",
21
- )
22
- parser.add_argument(
23
- "--exclude-apps",
24
- type=str,
25
- nargs="*",
26
- default=["admin", "auth", "contenttypes", "sessions", "messages", "staticfiles"],
27
- help="Apps to exclude from documentation",
28
- )
29
- parser.add_argument("--pretty", action="store_true", help="Pretty print JSON output")
30
-
31
- def handle(self, *args, **options):
32
- output_file = options["output"]
33
- exclude_apps = options["exclude_apps"]
34
- pretty = options["pretty"]
35
-
36
- self.stdout.write(self.style.SUCCESS("🔍 Scanning Django models..."))
37
-
38
- model_docs = self.generate_model_documentation(exclude_apps)
39
-
40
- json_data = {
41
- "models": model_docs,
42
- "stats": {
43
- "total_models": len(model_docs),
44
- "total_apps": len({model["app_label"] for model in model_docs.values()}),
45
- },
46
- }
47
-
48
- output_path = Path(output_file)
49
- output_path.parent.mkdir(parents=True, exist_ok=True)
50
- with output_path.open("w", encoding="utf-8") as f:
51
- if pretty:
52
- json.dump(json_data, f, indent=2, ensure_ascii=False, default=str)
53
- else:
54
- json.dump(json_data, f, ensure_ascii=False, default=str)
55
-
56
- self.stdout.write(
57
- self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
58
- )
59
- self.stdout.write(f"📊 Total models: {len(model_docs)}")
60
- self.stdout.write(
61
- f"📦 Total apps: {len({model['app_label'] for model in model_docs.values()})}"
62
- )
63
-
64
- def generate_model_documentation(self, exclude_apps):
65
- """Generate documentation for all Django models"""
66
- model_docs = {}
67
-
68
- for app_config in apps.get_app_configs():
69
- app_label = app_config.label
70
-
71
- # Skip excluded apps
72
- if app_label in exclude_apps:
73
- self.stdout.write(f"⏭️ Skipping app: {app_label}")
74
- continue
75
-
76
- self.stdout.write(f"📱 Processing app: {app_label}")
77
-
78
- for model in app_config.get_models():
79
- model_name = model.__name__
80
- model_key = f"{app_label}.{model_name}"
81
-
82
- self.stdout.write(f" 📋 Processing model: {model_name}")
83
-
84
- model_docs[model_key] = self.introspect_model(model, app_label)
85
-
86
- return model_docs
87
-
88
- def introspect_model(self, model, app_label):
89
- """Introspect a single Django model"""
90
- meta = model._meta
91
-
92
- # Basic model information
93
- model_doc = {
94
- "name": model.__name__,
95
- "app_label": app_label,
96
- "table_name": meta.db_table,
97
- "verbose_name": str(meta.verbose_name),
98
- "verbose_name_plural": str(meta.verbose_name_plural),
99
- "description": self.get_model_description(model),
100
- "abstract": meta.abstract,
101
- "proxy": meta.proxy,
102
- "fields": {},
103
- "relationships": {},
104
- "meta_options": self.get_meta_options(meta),
105
- "methods": self.get_model_methods(model),
106
- }
107
-
108
- # Process fields
109
- for field in meta.get_fields():
110
- if field.many_to_many or field.one_to_many or field.many_to_one or field.one_to_one:
111
- # Handle relationships separately
112
- model_doc["relationships"][field.name] = self.introspect_relationship(field)
113
- else:
114
- # Handle regular fields
115
- model_doc["fields"][field.name] = self.introspect_field(field)
116
-
117
- return model_doc
118
-
119
- def introspect_field(self, field):
120
- """Introspect a single model field"""
121
- return {
122
- "name": field.name,
123
- "type": field.__class__.__name__,
124
- "verbose_name": (
125
- str(field.verbose_name) if hasattr(field, "verbose_name") else field.name
126
- ),
127
- "help_text": field.help_text if hasattr(field, "help_text") else "",
128
- "null": getattr(field, "null", False),
129
- "blank": getattr(field, "blank", False),
130
- "editable": getattr(field, "editable", True),
131
- "primary_key": getattr(field, "primary_key", False),
132
- "unique": getattr(field, "unique", False),
133
- "db_index": getattr(field, "db_index", False),
134
- "default": self.get_field_default(field),
135
- "choices": self.get_field_choices(field),
136
- "validators": self.get_field_validators(field),
137
- "field_specific": self.get_field_specific_attrs(field),
138
- }
139
-
140
- def introspect_relationship(self, field):
141
- """Introspect relationship fields"""
142
- return {
143
- "name": field.name,
144
- "type": field.__class__.__name__,
145
- "related_model": f"{field.related_model._meta.app_label}."
146
- f"{field.related_model.__name__}",
147
- "related_name": getattr(field, "related_name", None),
148
- "on_delete": self.get_on_delete_name(field),
149
- "null": getattr(field, "null", False),
150
- "blank": getattr(field, "blank", False),
151
- "many_to_many": field.many_to_many,
152
- "one_to_many": field.one_to_many,
153
- "many_to_one": field.many_to_one,
154
- "one_to_one": field.one_to_one,
155
- }
156
-
157
- def get_on_delete_name(self, field):
158
- """Get readable name for on_delete option"""
159
- if not hasattr(field, "on_delete") or field.on_delete is None:
160
- return None
161
-
162
- # Import Django's on_delete functions
163
-
164
- # Map function objects to their readable names
165
- on_delete_mapping = {
166
- models.CASCADE: "CASCADE",
167
- models.PROTECT: "PROTECT",
168
- models.SET_NULL: "SET_NULL",
169
- models.SET_DEFAULT: "SET_DEFAULT",
170
- models.SET: "SET",
171
- models.DO_NOTHING: "DO_NOTHING",
172
- models.RESTRICT: "RESTRICT",
173
- }
174
-
175
- on_delete_func = field.on_delete
176
-
177
- # Handle SET() callable
178
- if hasattr(on_delete_func, "__name__") and on_delete_func.__name__ == "SET":
179
- return "SET"
180
-
181
- # Check if it's one of the standard Django on_delete options
182
- for func, name in on_delete_mapping.items():
183
- if on_delete_func is func:
184
- return name
185
-
186
- # Fallback for custom functions or unknown cases
187
- return getattr(on_delete_func, "__name__", str(on_delete_func))
188
-
189
- def get_model_description(self, model):
190
- """Get model description from docstring or generate one"""
191
- if model.__doc__ and not model.__doc__.startswith(model.__name__ + "("):
192
- # Only use docstring if it's not just the auto-generated field list
193
- return model.__doc__.strip()
194
- return ""
195
-
196
- def get_meta_options(self, meta):
197
- """Extract Meta class options"""
198
- options = {}
199
-
200
- # Common Meta options
201
- meta_attrs = [
202
- "ordering",
203
- "unique_together",
204
- "index_together",
205
- "constraints",
206
- "indexes",
207
- "permissions",
208
- "default_permissions",
209
- "get_latest_by",
210
- "order_with_respect_to",
211
- "managed",
212
- "default_manager_name",
213
- ]
214
-
215
- for attr in meta_attrs:
216
- if hasattr(meta, attr):
217
- value = getattr(meta, attr)
218
- if value:
219
- options[attr] = str(value)
220
-
221
- return options
222
-
223
- def get_model_methods(self, model):
224
- """Get custom model methods (excluding built-in Django methods)"""
225
- methods = []
226
-
227
- # Get all methods that don't start with underscore and aren't Django built-ins
228
- django_methods = {
229
- "save",
230
- "delete",
231
- "clean",
232
- "full_clean",
233
- "validate_unique",
234
- "get_absolute_url",
235
- "get_next_by_",
236
- "get_previous_by_",
237
- "refresh_from_db",
238
- "serializable_value",
239
- "check",
240
- "from_db",
241
- "clean_fields",
242
- "get_deferred_fields",
243
- "pk",
244
- }
245
-
246
- model_field_names = {field.name for field in model._meta.get_fields()}
247
-
248
- for attr_name in dir(model):
249
- if (
250
- not attr_name.startswith("_")
251
- and not attr_name.startswith("get_next_by_")
252
- and not attr_name.startswith("get_previous_by_")
253
- and attr_name not in django_methods
254
- and callable(getattr(model, attr_name))
255
- ):
256
- display_method_match = re.match(r"^get_(.+)_display$", attr_name)
257
- if display_method_match:
258
- field_name = display_method_match.group(1)
259
- if field_name in model_field_names:
260
- # Exclude if the field doesn't exist in the model
261
- continue
262
-
263
- method = getattr(model, attr_name)
264
-
265
- # Check if it's a method defined in this class (not inherited from Django)
266
- if (
267
- inspect.ismethod(method)
268
- or inspect.isfunction(method)
269
- or (hasattr(method, "__func__") and inspect.isfunction(method.__func__))
270
- ):
271
- # Check if method is defined in the model class itself
272
- if hasattr(model, attr_name) and attr_name in model.__dict__:
273
- methods.append(
274
- {
275
- "name": attr_name,
276
- "docstring": method.__doc__.strip() if method.__doc__ else "",
277
- }
278
- )
279
-
280
- return methods
281
-
282
- def get_field_default(self, field):
283
- """Get field default value"""
284
- if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
285
- default = field.default
286
- if callable(default):
287
- return f"<callable: {default.__name__}>"
288
- return str(default)
289
- return None
290
-
291
- def get_field_choices(self, field):
292
- """Get field choices"""
293
- if hasattr(field, "choices") and field.choices:
294
- return [{"value": choice[0], "display": choice[1]} for choice in field.choices]
295
- return []
296
-
297
- def get_field_validators(self, field):
298
- """Get field validators"""
299
- if hasattr(field, "validators") and field.validators:
300
- return [validator.__class__.__name__ for validator in field.validators]
301
- return []
302
-
303
- def get_field_specific_attrs(self, field):
304
- """Get field-specific attributes"""
305
- specific_attrs = {}
306
-
307
- # CharField, TextField
308
- if hasattr(field, "max_length") and field.max_length:
309
- specific_attrs["max_length"] = field.max_length
310
-
311
- # DecimalField
312
- if hasattr(field, "max_digits") and field.max_digits:
313
- specific_attrs["max_digits"] = field.max_digits
314
- if hasattr(field, "decimal_places") and field.decimal_places:
315
- specific_attrs["decimal_places"] = field.decimal_places
316
-
317
- # FileField, ImageField
318
- if hasattr(field, "upload_to") and field.upload_to:
319
- specific_attrs["upload_to"] = str(field.upload_to)
320
-
321
- # DateTimeField
322
- if hasattr(field, "auto_now") and field.auto_now:
323
- specific_attrs["auto_now"] = field.auto_now
324
- if hasattr(field, "auto_now_add") and field.auto_now_add:
325
- specific_attrs["auto_now_add"] = field.auto_now_add
326
-
327
- return specific_attrs
1
+ import inspect
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from django.apps import apps
7
+ from django.core.management.base import BaseCommand
8
+ from django.db import models
9
+
10
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
11
+
12
+ class Command(BaseCommand):
13
+ help = "Generate model documentation JSON from Django model introspection"
14
+
15
+ def add_arguments(self, parser):
16
+ parser.add_argument(
17
+ "--output",
18
+ type=str,
19
+ default=drf_to_mkdoc_settings.MODEL_DOCS_FILE,
20
+ help=f"Output JSON file name (default: {drf_to_mkdoc_settings.MODEL_DOCS_FILE})",
21
+ )
22
+ parser.add_argument(
23
+ "--exclude-apps",
24
+ type=str,
25
+ nargs="*",
26
+ default=["admin", "auth", "contenttypes", "sessions", "messages", "staticfiles"],
27
+ help="Apps to exclude from documentation",
28
+ )
29
+ parser.add_argument("--pretty", action="store_true", help="Pretty print JSON output")
30
+
31
+ def handle(self, *args, **options):
32
+ output_file = options["output"]
33
+ exclude_apps = options["exclude_apps"]
34
+ pretty = options["pretty"]
35
+
36
+ self.stdout.write(self.style.SUCCESS("🔍 Scanning Django models..."))
37
+
38
+ model_docs = self.generate_model_documentation(exclude_apps)
39
+
40
+ json_data = {
41
+ "models": model_docs,
42
+ "stats": {
43
+ "total_models": len(model_docs),
44
+ "total_apps": len({model["app_label"] for model in model_docs.values()}),
45
+ },
46
+ }
47
+
48
+ output_path = Path(output_file)
49
+ output_path.parent.mkdir(parents=True, exist_ok=True)
50
+ with output_path.open("w", encoding="utf-8") as f:
51
+ if pretty:
52
+ json.dump(json_data, f, indent=2, ensure_ascii=False, default=str)
53
+ else:
54
+ json.dump(json_data, f, ensure_ascii=False, default=str)
55
+
56
+ self.stdout.write(
57
+ self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
58
+ )
59
+ self.stdout.write(f"📊 Total models: {len(model_docs)}")
60
+ self.stdout.write(
61
+ f"📦 Total apps: {len({model['app_label'] for model in model_docs.values()})}"
62
+ )
63
+
64
+ def generate_model_documentation(self, exclude_apps):
65
+ """Generate documentation for all Django models"""
66
+ model_docs = {}
67
+
68
+ for app_config in apps.get_app_configs():
69
+ app_label = app_config.label
70
+
71
+ # Skip excluded apps
72
+ if app_label in exclude_apps:
73
+ self.stdout.write(f"⏭️ Skipping app: {app_label}")
74
+ continue
75
+
76
+ self.stdout.write(f"📱 Processing app: {app_label}")
77
+
78
+ for model in app_config.get_models():
79
+ model_name = model.__name__
80
+ model_key = f"{app_label}.{model_name}"
81
+
82
+ self.stdout.write(f" 📋 Processing model: {model_name}")
83
+
84
+ model_docs[model_key] = self.introspect_model(model, app_label)
85
+
86
+ return model_docs
87
+
88
+ def introspect_model(self, model, app_label):
89
+ """Introspect a single Django model"""
90
+ meta = model._meta
91
+
92
+ # Basic model information
93
+ model_doc = {
94
+ "name": model.__name__,
95
+ "app_label": app_label,
96
+ "table_name": meta.db_table,
97
+ "verbose_name": str(meta.verbose_name),
98
+ "verbose_name_plural": str(meta.verbose_name_plural),
99
+ "description": self.get_model_description(model),
100
+ "abstract": meta.abstract,
101
+ "proxy": meta.proxy,
102
+ "fields": {},
103
+ "relationships": {},
104
+ "meta_options": self.get_meta_options(meta),
105
+ "methods": self.get_model_methods(model),
106
+ }
107
+
108
+ # Process fields
109
+ for field in meta.get_fields():
110
+ if field.many_to_many or field.one_to_many or field.many_to_one or field.one_to_one:
111
+ # Handle relationships separately
112
+ model_doc["relationships"][field.name] = self.introspect_relationship(field)
113
+ else:
114
+ # Handle regular fields
115
+ model_doc["fields"][field.name] = self.introspect_field(field)
116
+
117
+ return model_doc
118
+
119
+ def introspect_field(self, field):
120
+ """Introspect a single model field"""
121
+ return {
122
+ "name": field.name,
123
+ "type": field.__class__.__name__,
124
+ "verbose_name": (
125
+ str(field.verbose_name) if hasattr(field, "verbose_name") else field.name
126
+ ),
127
+ "help_text": field.help_text if hasattr(field, "help_text") else "",
128
+ "null": getattr(field, "null", False),
129
+ "blank": getattr(field, "blank", False),
130
+ "editable": getattr(field, "editable", True),
131
+ "primary_key": getattr(field, "primary_key", False),
132
+ "unique": getattr(field, "unique", False),
133
+ "db_index": getattr(field, "db_index", False),
134
+ "default": self.get_field_default(field),
135
+ "choices": self.get_field_choices(field),
136
+ "validators": self.get_field_validators(field),
137
+ "field_specific": self.get_field_specific_attrs(field),
138
+ }
139
+
140
+ def introspect_relationship(self, field):
141
+ """Introspect relationship fields"""
142
+ return {
143
+ "name": field.name,
144
+ "type": field.__class__.__name__,
145
+ "related_model": f"{field.related_model._meta.app_label}."
146
+ f"{field.related_model.__name__}",
147
+ "related_name": getattr(field, "related_name", None),
148
+ "on_delete": self.get_on_delete_name(field),
149
+ "null": getattr(field, "null", False),
150
+ "blank": getattr(field, "blank", False),
151
+ "many_to_many": field.many_to_many,
152
+ "one_to_many": field.one_to_many,
153
+ "many_to_one": field.many_to_one,
154
+ "one_to_one": field.one_to_one,
155
+ }
156
+
157
+ def get_on_delete_name(self, field):
158
+ """Get readable name for on_delete option"""
159
+ if not hasattr(field, "on_delete") or field.on_delete is None:
160
+ return None
161
+
162
+ # Import Django's on_delete functions
163
+
164
+ # Map function objects to their readable names
165
+ on_delete_mapping = {
166
+ models.CASCADE: "CASCADE",
167
+ models.PROTECT: "PROTECT",
168
+ models.SET_NULL: "SET_NULL",
169
+ models.SET_DEFAULT: "SET_DEFAULT",
170
+ models.SET: "SET",
171
+ models.DO_NOTHING: "DO_NOTHING",
172
+ models.RESTRICT: "RESTRICT",
173
+ }
174
+
175
+ on_delete_func = field.on_delete
176
+
177
+ # Handle SET() callable
178
+ if hasattr(on_delete_func, "__name__") and on_delete_func.__name__ == "SET":
179
+ return "SET"
180
+
181
+ # Check if it's one of the standard Django on_delete options
182
+ for func, name in on_delete_mapping.items():
183
+ if on_delete_func is func:
184
+ return name
185
+
186
+ # Fallback for custom functions or unknown cases
187
+ return getattr(on_delete_func, "__name__", str(on_delete_func))
188
+
189
+ def get_model_description(self, model):
190
+ """Get model description from docstring or generate one"""
191
+ if model.__doc__ and not model.__doc__.startswith(model.__name__ + "("):
192
+ # Only use docstring if it's not just the auto-generated field list
193
+ return model.__doc__.strip()
194
+ return ""
195
+
196
+ def get_meta_options(self, meta):
197
+ """Extract Meta class options"""
198
+ options = {}
199
+
200
+ # Common Meta options
201
+ meta_attrs = [
202
+ "ordering",
203
+ "unique_together",
204
+ "index_together",
205
+ "constraints",
206
+ "indexes",
207
+ "permissions",
208
+ "default_permissions",
209
+ "get_latest_by",
210
+ "order_with_respect_to",
211
+ "managed",
212
+ "default_manager_name",
213
+ ]
214
+
215
+ for attr in meta_attrs:
216
+ if hasattr(meta, attr):
217
+ value = getattr(meta, attr)
218
+ if value:
219
+ options[attr] = str(value)
220
+
221
+ return options
222
+
223
+ def get_model_methods(self, model):
224
+ """Get custom model methods (excluding built-in Django methods)"""
225
+ methods = []
226
+
227
+ # Get all methods that don't start with underscore and aren't Django built-ins
228
+ django_methods = {
229
+ "save",
230
+ "delete",
231
+ "clean",
232
+ "full_clean",
233
+ "validate_unique",
234
+ "get_absolute_url",
235
+ "get_next_by_",
236
+ "get_previous_by_",
237
+ "refresh_from_db",
238
+ "serializable_value",
239
+ "check",
240
+ "from_db",
241
+ "clean_fields",
242
+ "get_deferred_fields",
243
+ "pk",
244
+ }
245
+
246
+ model_field_names = {field.name for field in model._meta.get_fields()}
247
+
248
+ for attr_name in dir(model):
249
+ if (
250
+ not attr_name.startswith("_")
251
+ and not attr_name.startswith("get_next_by_")
252
+ and not attr_name.startswith("get_previous_by_")
253
+ and attr_name not in django_methods
254
+ and callable(getattr(model, attr_name))
255
+ ):
256
+ display_method_match = re.match(r"^get_(.+)_display$", attr_name)
257
+ if display_method_match:
258
+ field_name = display_method_match.group(1)
259
+ if field_name in model_field_names:
260
+ # Exclude if the field doesn't exist in the model
261
+ continue
262
+
263
+ method = getattr(model, attr_name)
264
+
265
+ # Check if it's a method defined in this class (not inherited from Django)
266
+ if (
267
+ inspect.ismethod(method)
268
+ or inspect.isfunction(method)
269
+ or (hasattr(method, "__func__") and inspect.isfunction(method.__func__))
270
+ ):
271
+ # Check if method is defined in the model class itself
272
+ if hasattr(model, attr_name) and attr_name in model.__dict__:
273
+ methods.append(
274
+ {
275
+ "name": attr_name,
276
+ "docstring": method.__doc__.strip() if method.__doc__ else "",
277
+ }
278
+ )
279
+
280
+ return methods
281
+
282
+ def get_field_default(self, field):
283
+ """Get field default value"""
284
+ if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
285
+ default = field.default
286
+ if callable(default):
287
+ return f"<callable: {default.__name__}>"
288
+ return str(default)
289
+ return None
290
+
291
+ def get_field_choices(self, field):
292
+ """Get field choices"""
293
+ if hasattr(field, "choices") and field.choices:
294
+ return [{"value": choice[0], "display": choice[1]} for choice in field.choices]
295
+ return []
296
+
297
+ def get_field_validators(self, field):
298
+ """Get field validators"""
299
+ if hasattr(field, "validators") and field.validators:
300
+ return [validator.__class__.__name__ for validator in field.validators]
301
+ return []
302
+
303
+ def get_field_specific_attrs(self, field):
304
+ """Get field-specific attributes"""
305
+ specific_attrs = {}
306
+
307
+ # CharField, TextField
308
+ if hasattr(field, "max_length") and field.max_length:
309
+ specific_attrs["max_length"] = field.max_length
310
+
311
+ # DecimalField
312
+ if hasattr(field, "max_digits") and field.max_digits:
313
+ specific_attrs["max_digits"] = field.max_digits
314
+ if hasattr(field, "decimal_places") and field.decimal_places:
315
+ specific_attrs["decimal_places"] = field.decimal_places
316
+
317
+ # FileField, ImageField
318
+ if hasattr(field, "upload_to") and field.upload_to:
319
+ specific_attrs["upload_to"] = str(field.upload_to)
320
+
321
+ # DateTimeField
322
+ if hasattr(field, "auto_now") and field.auto_now:
323
+ specific_attrs["auto_now"] = field.auto_now
324
+ if hasattr(field, "auto_now_add") and field.auto_now_add:
325
+ specific_attrs["auto_now_add"] = field.auto_now_add
326
+
327
+ return specific_attrs