Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding list_fields command #1262

Merged
merged 20 commits into from Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
88e50dc
A simple command to list out all fields for installed apps
JackAtOmenApps Oct 22, 2018
906918a
Fixed formatting per PEP8
JackAtOmenApps Oct 22, 2018
8360967
Removed trailing whitespace
JackAtOmenApps Oct 22, 2018
5606e2d
Re-added single newline at end of file
JackAtOmenApps Oct 24, 2018
a1930db
Removed trailing whitespace in commend_extensions.rst
JackAtOmenApps Oct 24, 2018
103e27e
Merge remote-tracking branch 'django-extensions/master'
JackAtOmenApps Mar 18, 2020
29c6897
Minor changes to improve output formatting.
JackAtOmenApps Mar 18, 2020
5fafc5e
Modified command name and added listing of each model's methods
JackAtOmenApps Mar 19, 2020
1d1fd79
Corrected order for entry of command in the docs
JackAtOmenApps Mar 19, 2020
0448545
Removed extra spaces on line endings.
JackAtOmenApps Mar 19, 2020
ea1ac6d
Implemented @trbs recommendations, added settings, and made improvements
JackAtOmenApps Mar 25, 2020
753c8c7
Corrected flake 8 issues and formatted with black.
JackAtOmenApps Mar 26, 2020
b7cfa5b
Fixed line endings.
JackAtOmenApps Mar 26, 2020
f8caeed
Made it possible to use both db-type and field-class together
JackAtOmenApps Mar 26, 2020
2d0c63d
Added tests
JackAtOmenApps Mar 27, 2020
c0f39a0
Fixed trailing whitespace
JackAtOmenApps Mar 27, 2020
5d11cb9
Corrected for representation in TravisCI
JackAtOmenApps Mar 27, 2020
1b8a438
Minor changes to improve output formatting.
JackAtOmenApps Mar 19, 2020
aea57a1
Implemented @trbs recommendations, added settings, and made improvements
JackAtOmenApps Apr 2, 2020
ba77c9d
Merge remote-tracking branch 'origin/master'
JackAtOmenApps Apr 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
149 changes: 149 additions & 0 deletions django_extensions/management/commands/list_model_info.py
@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
# Author: OmenApps. http://www.omenapps.com
import inspect

from django.apps import apps as django_apps
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connection
from django_extensions.management.color import color_style
from django_extensions.management.utils import signalcommand

TAB = " "
HALFTAB = " "


class Command(BaseCommand):
"""A simple management command which lists model fields and methods."""

help = "List out the fields and methods for each model"

def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument("--field-class", action="store_true", default=None, help="show class name of field.")
parser.add_argument("--db-type", action="store_true", default=None, help="show database column type of field.")
parser.add_argument("--signature", action="store_true", default=None, help="show the signature of method.")
parser.add_argument(
"--all-methods", action="store_true", default=None, help="list all methods, including private and default."
)
parser.add_argument(
"--model",
nargs="?",
type=str,
default=None,
help="list the details for a single model. Input should be in the form appname.Modelname",
)

def list_model_info(self, options):

style = color_style()
INFO = getattr(style, "INFO", lambda x: x)
WARN = getattr(style, "WARN", lambda x: x)
BOLD = getattr(style, "BOLD", lambda x: x)

FIELD_CLASS = (
True if options.get("field_class", None) is not None else getattr(settings, "MODEL_INFO_FIELD_CLASS", False)
)
DB_TYPE = True if options.get("db_type", None) is not None else getattr(settings, "MODEL_INFO_DB_TYPE", False)
SIGNATURE = (
True if options.get("signature", None) is not None else getattr(settings, "MODEL_INFO_SIGNATURE", False)
)
ALL_METHODS = (
True if options.get("all_methods", None) is not None else getattr(settings, "MODEL_INFO_ALL_METHODS", False)
)
MODEL = (
options.get("model")
if options.get("model", None) is not None
else getattr(settings, "MODEL_INFO_MODEL", False)
)

default_methods = [
"check",
"clean",
"clean_fields",
"date_error_message",
"delete",
"from_db",
"full_clean",
"get_absolute_url",
"get_deferred_fields",
"prepare_database_save",
"refresh_from_db",
"save",
"save_base",
"serializable_value",
"unique_error_message",
"validate_unique",
]

if MODEL:
model_list = [django_apps.get_model(MODEL)]
else:
model_list = sorted(
django_apps.get_models(), key=lambda x: (x._meta.app_label, x._meta.object_name), reverse=False
)
for model in model_list:
self.stdout.write(INFO(model._meta.app_label + "." + model._meta.object_name))
self.stdout.write(BOLD(HALFTAB + "Fields:"))

for field in model._meta.get_fields():
field_info = TAB + field.name + " -"

if FIELD_CLASS:
try:
field_info += " " + field.__class__.__name__
except TypeError:
field_info += (WARN(" TypeError (field_class)"))
except AttributeError:
field_info += (WARN(" AttributeError (field_class)"))
if FIELD_CLASS and DB_TYPE:
field_info += ","
if DB_TYPE:
try:
field_info += " " + field.db_type(connection=connection)
except TypeError:
field_info += (WARN(" TypeError (db_type)"))
except AttributeError:
field_info += (WARN(" AttributeError (db_type)"))

self.stdout.write(field_info)

if ALL_METHODS:
self.stdout.write(BOLD(HALFTAB + "Methods (all):"))
else:
self.stdout.write(BOLD(HALFTAB + "Methods (non-private/internal):"))

for method_name in dir(model):
try:
method = getattr(model, method_name)
if ALL_METHODS:
if callable(method) and not method_name[0].isupper():
if SIGNATURE:
signature = inspect.signature(method)
else:
signature = "()"
self.stdout.write(TAB + method_name + str(signature))
else:
if (
callable(method)
and not method_name.startswith("_")
and method_name not in default_methods
and not method_name[0].isupper()
):
if SIGNATURE:
signature = inspect.signature(method)
else:
signature = "()"
self.stdout.write(TAB + method_name + str(signature))
except AttributeError:
self.stdout.write(TAB + method_name + WARN(" - AttributeError"))
except ValueError:
self.stdout.write(TAB + method_name + WARN(" - ValueError (could not identify signature)"))

self.stdout.write("\n")

self.stdout.write(INFO("Total Models Listed: %d" % len(model_list)))

@signalcommand
def handle(self, *args, **options):
self.list_model_info(options)
5 changes: 5 additions & 0 deletions docs/command_extensions.rst
Expand Up @@ -50,6 +50,10 @@ Current Command Extensions
to send this output to a file yourself. Great for graphing your models. Pass
multiple application names to combine all the models into a single dot file.

* `list_model_info`_ - Lists out all the fields and methods for models in installed apps.
This is helpful when you don't remember how to refer to a related field or want to quickly identify
the fields and methods available in a particular model.

* *mail_debug* - Starts a mail server which echos out the contents of the email
instead of sending it.

Expand Down Expand Up @@ -128,6 +132,7 @@ Current Command Extensions

.. _`export_emails`: export_emails.html
.. _`graph_models`: graph_models.html
.. _`list_model_info`: list_model_info.html
.. _`print_settings`: print_settings.html
.. _`runscript`: runscript.html
.. _`runserver_plus`: runserver_plus.html
Expand Down
85 changes: 85 additions & 0 deletions docs/list_model_info.rst
@@ -0,0 +1,85 @@
list_model_info
===============

:synopsis: Lists out all the fields and methods for models in installed apps.

Introduction
------------

When working with large projects or when returning to a code base after some time away, it can be challenging to remember all of the
fields and methods associated with your models. This command makes it easy to see:

* what fields are available
* how they are refered to in queries
* each field's class
* each field's representation in the database
* what methods are available
* method signatures


Commandline arguments
^^^^^^^^^^^^^^^^^^^^^
You can configure the output in a number of ways.

::

# Show each field's class
$ ./manage.py list_model_info --field-class

::

# Show each field's database type representation
$ ./manage.py list_model_info --db-type


::

# Show each method's signature
$ ./manage.py list_model_info --signature

::

# Show all model methods, including private methods and django's default methods
$ ./manage.py list_model_info --all-methods

::

# Output only information for a single model, specifying the app and model using dot notation
$ ./manage.py list_model_info --model users.User


You can combine arguments. for instance, to list all methods and show the method signatures for the User model within the users app::

$ ./manage.py list_model_info --all --signature --model users.User



Settings Configuration
^^^^^^^^^^^^^^^^^^^^^^

You can specify default values in your settings.py to simplify running this command.


.. tip::
Commandline arguments override the following settings, allowing you to change options on the fly.


To show each field's class::

MODEL_INFO_FIELD_CLASS = True

To show each field's database type representation::

MODEL_INFO_DB_TYPE = True

To show each method's signature::

MODEL_INFO_SIGNATURE = True

To show all model methods, including private methods and django's default methods::

MODEL_INFO_ALL_METHODS = True

To output only information for a single model, specify the app and model using dot notation::

MODEL_INFO_MODEL = 'users.User'
57 changes: 57 additions & 0 deletions tests/test_management_command.py
Expand Up @@ -262,6 +262,63 @@ def test_no_color(self):
self.assertNotIn('\x1b', self.output)


class ListModelInfoTests(TestCase):
"""
Tests for the `list_model_info` management command.
"""
def test_plain(self):
out = StringIO()
call_command('list_model_info', '--model', 'django_extensions.MultipleFieldsAndMethods', stdout=out)
self.output = out.getvalue()
self.assertIn('id', self.output)
self.assertIn('char_field', self.output)
self.assertIn('integer_field', self.output)
self.assertIn('foreign_key_field', self.output)
self.assertIn('has_self_only()', self.output)
self.assertIn('has_one_extra_argument()', self.output)
self.assertIn('has_two_extra_arguments()', self.output)
self.assertIn('has_args_kwargs()', self.output)
self.assertIn('has_defaults()', self.output)
self.assertNotIn('__class__()', self.output)
self.assertNotIn('validate_unique()', self.output)

def test_all(self):
out = StringIO()
call_command('list_model_info', '--model', 'django_extensions.MultipleFieldsAndMethods', '--all', stdout=out)
self.output = out.getvalue()
self.assertIn('id', self.output)
self.assertIn('__class__()', self.output)
self.assertIn('validate_unique()', self.output)

def test_signature(self):
out = StringIO()
call_command('list_model_info', '--model', 'django_extensions.MultipleFieldsAndMethods', '--signature', stdout=out)
self.output = out.getvalue()
self.assertIn('has_self_only(self)', self.output)
self.assertIn('has_one_extra_argument(self, arg_one)', self.output)
self.assertIn('has_two_extra_arguments(self, arg_one, arg_two)', self.output)
self.assertIn('has_args_kwargs(self, *args, **kwargs)', self.output)
self.assertIn("has_defaults(self, one=1, two='Two', true=True, false=False, none=None)", self.output)

def test_db_type(self):
out = StringIO()
call_command('list_model_info', '--model', 'django_extensions.MultipleFieldsAndMethods', '--db-type', stdout=out)
self.output = out.getvalue()
self.assertIn('id - serial', self.output)
self.assertIn('char_field - varchar(10)', self.output)
self.assertIn('integer_field - integer', self.output)
self.assertIn('foreign_key_field - integer', self.output)

def test_field_class(self):
out = StringIO()
call_command('list_model_info', '--model', 'django_extensions.MultipleFieldsAndMethods', '--field-class', stdout=out)
self.output = out.getvalue()
self.assertIn('id - AutoField', self.output)
self.assertIn('char_field - CharField', self.output)
self.assertIn('integer_field - IntegerField', self.output)
self.assertIn('foreign_key_field - ForeignKey', self.output)


class MergeModelInstancesTests(TestCase):
"""
Tests for the `merge_model_instances` management command.
Expand Down
24 changes: 24 additions & 0 deletions tests/testapp/models.py
Expand Up @@ -466,3 +466,27 @@ class HasOwnerModel(models.Model):

class Meta:
app_label = 'django_extensions'


class MultipleFieldsAndMethods(models.Model):
char_field = models.CharField(max_length=10)
integer_field = models.IntegerField()
foreign_key_field = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)

def has_self_only(self):
pass

def has_one_extra_argument(self, arg_one):
pass

def has_two_extra_arguments(self, arg_one, arg_two):
pass

def has_args_kwargs(self, *args, **kwargs):
pass

def has_defaults(self, one=1, two='Two', true=True, false=False, none=None):
pass

class Meta:
app_label = 'django_extensions'