仿照django自帶的admin實現本身的admin組件
https://docs.djangoproject.com/zh-hans/2.2/ref/contrib/admin/html
運行python manage.py runserver
啓動
打開manage.py
文件能夠發現該文件代碼很是簡單,主要乾了兩件事情:python
settings.py
文件的路徑存入系統環境變量os.environ
。os.environ
是一個繼承自抽象類_collections_abc.MutableMapping
的os._Environ
類的對象。sys.argv
傳給execute_from_command_line
執行。execute_from_command_line
使用argv
建立一個ManagementUtility
對象,def main(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Django_Admin.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main()
接着進入execute_from_command_line
,execute_from_command_line
使用argv
建立一個ManagementUtility
對象,而後調用其execute
方法。git
# django/core/management/__init__.py def execute_from_command_line(argv=None): """Run a ManagementUtility.""" utility = ManagementUtility(argv) utility.execute()
在這時,咱們發現因爲當前文件import了其餘依賴而初試化了apps
對象和 settings
對象.sql
因爲Import
了django.apps.apps
,apps
對象會被初始化,這個對象主要負責存儲已安裝的django app
的配置(AppConfig
:包括app路徑,名稱,模塊,app_label,顯示名稱等)以及每一個app
下的各個model
.django
# django/core/management/__init__.py from django.apps import apps from django.conf import settings
# django/apps/registry.py apps = Apps(installed_apps=None)
因爲import
了django.conf.settings
而初始化LazySettings
類,它繼承自LazyObject
類,LazyObject
經過內嵌函數代理了__bytes__
,__str__
,__len__
,__contains__
,__iter__
,__delitem__
,__setitem__
,__getitem__
,__hash__
,__ne__
,__eq__
,__class__
,__dir__
,__bool__
方法,使這些方法在被調用以前檢查setting
是否被加載,若是沒有的話,調用self._setup()
安裝,而後再執行被調用的方法.也就是說全部繼承LazyObject
的類,在調用上面方法時,會先調用_setup
方法,只須要將初始化setting
的代碼放到_setup
裏,就能夠實現懶加載setting
.app
# django/conf/__init__.py settings = LazySettings() # django/utils/functional.py def new_method_proxy(func): def inner(self, *args): if self._wrapped is empty: self._setup() return func(self._wrapped, *args) return inner # django/utils/functional.py class LazyObject: ... # Need to pretend to be the wrapped class, for the sake of objects that # care about this (especially in equality tests) __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) __eq__ = new_method_proxy(operator.eq) __lt__ = new_method_proxy(operator.lt) __gt__ = new_method_proxy(operator.gt) __ne__ = new_method_proxy(operator.ne) __hash__ = new_method_proxy(hash) # List/Tuple/Dictionary methods support __getitem__ = new_method_proxy(operator.getitem) __setitem__ = new_method_proxy(operator.setitem) __delitem__ = new_method_proxy(operator.delitem) __iter__ = new_method_proxy(iter) __len__ = new_method_proxy(len) __contains__ = new_method_proxy(operator.contains) ...
繼續看ManagementUtility
,它的execute
方法主要完成了,命令行參數解析
和運行command
socket
ArgumentParser
的CommandParser
類,而後經過CommandParser
類對參數進行解析。咱們在編寫django command
時繼承的BaseCommand
就是經過CommandParser
來解析命令行參數的。command
。解析出subcommand
,分別根據subcommand
經過fetch_command
函數獲得各類command
對象,而command
對象是經過獲取django\core\management\commands
目錄和每一個app
下management\commands
下全部py
文件中的Command
類獲得。調用django.setup()
對logging
進行配置並調用apps.populate(settings.INSTALLED_APPS)
配置安裝app
。而後因爲Command
類都繼承自BaseCommand
,只須要調用run_from_argv
方法便可對子命令行參數執行。# django/core/management/__init__.py def execute(self): """ Given the command-line arguments, figure out which subcommand is being run, create a parser appropriate to that command, and run it. """ try: subcommand = self.argv[1] except IndexError: subcommand = 'help' # Display help if no arguments were given. # Preprocess options to extract --settings and --pythonpath. # These options could affect the commands that are available, so they # must be processed early. parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False) parser.add_argument('--settings') parser.add_argument('--pythonpath') parser.add_argument('args', nargs='*') # catch-all try: options, args = parser.parse_known_args(self.argv[2:]) handle_default_options(options) except CommandError: pass # Ignore any option errors at this point. try: settings.INSTALLED_APPS except ImproperlyConfigured as exc: self.settings_exception = exc except ImportError as exc: self.settings_exception = exc if settings.configured: # Start the auto-reloading dev server even if the code is broken. # The hardcoded condition is a code smell but we can't rely on a # flag on the command class because we haven't located it yet. if subcommand == 'runserver' and '--noreload' not in self.argv: try: autoreload.check_errors(django.setup)() except Exception: # The exception will be raised later in the child process # started by the autoreloader. Pretend it didn't happen by # loading an empty list of applications. apps.all_models = defaultdict(OrderedDict) apps.app_configs = OrderedDict() apps.apps_ready = apps.models_ready = apps.ready = True # Remove options not compatible with the built-in runserver # (e.g. options for the contrib.staticfiles' runserver). # Changes here require manually testing as described in # #27522. _parser = self.fetch_command('runserver').create_parser('django', 'runserver') _options, _args = _parser.parse_known_args(self.argv[2:]) for _arg in _args: self.argv.remove(_arg) # In all other cases, django.setup() is required to succeed. else: django.setup() self.autocomplete() if subcommand == 'help': if '--commands' in args: sys.stdout.write(self.main_help_text(commands_only=True) + '\n') elif not options.args: sys.stdout.write(self.main_help_text() + '\n') else: self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0]) # Special-cases: We want 'django-admin --version' and # 'django-admin --help' to work, for backwards compatibility. elif subcommand == 'version' or self.argv[1:] == ['--version']: sys.stdout.write(django.get_version() + '\n') elif self.argv[1:] in (['--help'], ['-h']): sys.stdout.write(self.main_help_text() + '\n') else: self.fetch_command(subcommand).run_from_argv(self.argv)
autoreload.check_errors(django.setup)()
其實也是調用django.setup
方法ide
def setup(set_prefix=True): """ Configure the settings (this happens as a side effect of accessing the first setting), configure logging and populate the app registry. Set the thread-local urlresolvers script prefix if `set_prefix` is True. """ from django.apps import apps from django.conf import settings from django.urls import set_script_prefix from django.utils.log import configure_logging configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) if set_prefix: set_script_prefix( '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME ) apps.populate(settings.INSTALLED_APPS)
apps.populate(settings.INSTALLED_APPS)
將會根據settings.INSTALLED_APPS
中的配置查找app
,以後分別調用每一個app_config
的ready
方法。因爲apps.populate
是加了鎖防止重複執行的,因此每一個app
的app_config.ready
只會初始化一次,咱們若是有須要在django
啓動時執行一次的操做,能夠將其放入app_config.ready
中。函數
在調用app_config.ready
以前,會先將全部的models
加載進來。其經過MODELS_MODULE_NAME="models"; import_module(app_name,MODELS_MODULE_NAME)
的方式導入models
。fetch
def populate(self, installed_apps=None): """ Load application configurations and models. Import each application module and then each model module. It is thread-safe and idempotent, but not reentrant. """ if self.ready: return # populate() might be called by two threads in parallel on servers # that create threads before initializing the WSGI callable. with self._lock: if self.ready: return # An RLock prevents other threads from entering this section. The # compare and set operation below is atomic. if self.loading: # Prevent reentrant calls to avoid running AppConfig.ready() # methods twice. raise RuntimeError("populate() isn't reentrant") self.loading = True # Phase 1: initialize app configs and import app modules. for entry in installed_apps: if isinstance(entry, AppConfig): app_config = entry else: app_config = AppConfig.create(entry) if app_config.label in self.app_configs: raise ImproperlyConfigured( "Application labels aren't unique, " "duplicates: %s" % app_config.label) self.app_configs[app_config.label] = app_config app_config.apps = self # Check for duplicate app names. counts = Counter( app_config.name for app_config in self.app_configs.values()) duplicates = [ name for name, count in counts.most_common() if count > 1] if duplicates: raise ImproperlyConfigured( "Application names aren't unique, " "duplicates: %s" % ", ".join(duplicates)) self.apps_ready = True # Phase 2: import models modules. for app_config in self.app_configs.values(): app_config.import_models() self.clear_cache() self.models_ready = True # Phase 3: run ready() methods of app configs. for app_config in self.get_app_configs(): app_config.ready() self.ready = True self.ready_event.set()
def import_models(self): # Dictionary of models for this app, primarily maintained in the # 'all_models' attribute of the Apps this AppConfig is attached to. self.models = self.apps.all_models[self.label] if module_has_submodule(self.module, MODELS_MODULE_NAME): models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME) self.models_module = import_module(models_module_name)
看一下admin
的AdminConfig
class SimpleAdminConfig(AppConfig): """Simple AppConfig which does not do automatic discovery.""" default_site = 'django.contrib.admin.sites.AdminSite' name = 'django.contrib.admin' verbose_name = _("Administration") def ready(self): checks.register(check_dependencies, checks.Tags.admin) checks.register(check_admin_app, checks.Tags.admin) class AdminConfig(SimpleAdminConfig): """The default AppConfig for admin which does autodiscovery.""" def ready(self): super().ready() self.module.autodiscover()
自動掃描每一個app
下的admin
包, 將裏面的東西註冊到site = DefaultAdminSite()
中
site
是一個單例對象
def autodiscover(): autodiscover_modules('admin', register_to=site)
autodiscover_modules
函數自動加載每一個app
下的admin.py
文件
def autodiscover_modules(*args, **kwargs): """ Auto-discover INSTALLED_APPS modules and fail silently when not present. This forces an import on them to register any admin bits they may want. You may provide a register_to keyword parameter as a way to access a registry. This register_to object must have a _registry instance variable to access it. """ from django.apps import apps register_to = kwargs.get('register_to') for app_config in apps.get_app_configs(): for module_to_search in args: # Attempt to import the app's module. try: if register_to: before_import_registry = copy.copy(register_to._registry) import_module('%s.%s' % (app_config.name, module_to_search)) except Exception: # Reset the registry to the state before the last import # as this import will have to reoccur on the next request and # this could raise NotRegistered and AlreadyRegistered # exceptions (see #8245). if register_to: register_to._registry = before_import_registry # Decide whether to bubble up this error. If the app just # doesn't have the module in question, we can ignore the error # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(app_config.module, module_to_search): raise
def register(self, model_or_iterable, admin_class=None, **options): """ Register the given model(s) with the given admin class. The model(s) should be Model classes, not instances. If an admin class isn't given, use ModelAdmin (the default admin options). If keyword arguments are given -- e.g., list_display -- apply them as options to the admin class. If a model is already registered, raise AlreadyRegistered. If a model is abstract, raise ImproperlyConfigured. """ admin_class = admin_class or ModelAdmin if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model._meta.abstract: raise ImproperlyConfigured( 'The model %s is abstract, so it cannot be registered with admin.' % model.__name__ ) if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) # Ignore the registration if the model has been # swapped out. if not model._meta.swapped: # If we got **options then dynamically construct a subclass of # admin_class with those **options. if options: # For reasons I don't quite understand, without a __module__ # the created class appears to "live" in the wrong place, # which causes issues later on. options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self)
print(admin.site._registry) {<class 'django.contrib.auth.models.Group'>: <django.contrib.auth.admin.GroupAdmin object at 0x0000019BA62B9BE0>, <class 'django.contrib.auth.models.User'>: <django.contrib.auth.admin.UserAdmin object at 0x0000019BA62E9BA8>, <class 'book.models.Book'>: <django.contrib.admin.options.ModelAdmin object at 0x0000019BA62E9DD8>, <class 'book.models.Author'>: <django.contrib.admin.options.ModelAdmin object at 0x0000019BA62FDC50>}
execute
函數中self.fetch_command(subcommand).run_from_argv(self.argv)
爲重點,是用戶輸入的命令的真實入口
def fetch_command(self, subcommand): """ Try to fetch the given subcommand, printing a message with the appropriate command called from the command line (usually "django-admin" or "manage.py") if it can't be found. """ # Get commands outside of try block to prevent swallowing exceptions commands = get_commands() try: app_name = commands[subcommand] except KeyError: if os.environ.get('DJANGO_SETTINGS_MODULE'): # If `subcommand` is missing due to misconfigured settings, the # following line will retrigger an ImproperlyConfigured exception # (get_commands() swallows the original one) so the user is # informed about it. settings.INSTALLED_APPS else: sys.stderr.write("No Django settings specified.\n") possible_matches = get_close_matches(subcommand, commands) sys.stderr.write('Unknown command: %r' % subcommand) if possible_matches: sys.stderr.write('. Did you mean %s?' % possible_matches[0]) sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name) sys.exit(1) if isinstance(app_name, BaseCommand): # If the command is already loaded, use it directly. klass = app_name else: klass = load_command_class(app_name, subcommand) return klass
def load_command_class(app_name, name): """ Given a command name and an application name, return the Command class instance. Allow all errors raised by the import process (ImportError, AttributeError) to propagate. """ module = import_module('%s.management.commands.%s' % (app_name, name)) return module.Command()
其中fetch_command
根據subcommand
返回對應的命令處理文件,load_command_class
函數返回module.Command()
,在本例中就是self.fetch_command(subcommand)
就是runserver.Command()
命令處理文件放在django/core/management/commands
下面
打開django/core/management/commands/runserver.py
文件
Command
類繼承了BaseCommand
類, 查看BaseCommand
的run_from_argv
方法
def run_from_argv(self, argv): """ Set up any environment changes requested (e.g., Python path and Django settings), then run this command. If the command raises a ``CommandError``, intercept it and print it sensibly to stderr. If the ``--traceback`` option is present or the raised ``Exception`` is not ``CommandError``, raise it. """ self._called_from_command_line = True parser = self.create_parser(argv[0], argv[1]) options = parser.parse_args(argv[2:]) cmd_options = vars(options) # Move positional args out of options to mimic legacy optparse args = cmd_options.pop('args', ()) handle_default_options(options) try: self.execute(*args, **cmd_options) except Exception as e: if options.traceback or not isinstance(e, CommandError): raise # SystemCheckError takes care of its own formatting. if isinstance(e, SystemCheckError): self.stderr.write(str(e), lambda x: x) else: self.stderr.write('%s: %s' % (e.__class__.__name__, e)) sys.exit(1) finally: try: connections.close_all() except ImproperlyConfigured: # Ignore if connections aren't setup at this point (e.g. no # configured settings). pass
execute
函數返回的是handle
函數
def execute(self, *args, **options): """ Try to execute this command, performing system checks if needed (as controlled by the ``requires_system_checks`` attribute, except if force-skipped). """ if options['force_color'] and options['no_color']: raise CommandError("The --no-color and --force-color options can't be used together.") if options['force_color']: self.style = color_style(force_color=True) elif options['no_color']: self.style = no_style() self.stderr.style_func = None if options.get('stdout'): self.stdout = OutputWrapper(options['stdout']) if options.get('stderr'): self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func) if self.requires_system_checks and not options.get('skip_checks'): self.check() if self.requires_migrations_checks: self.check_migrations() output = self.handle(*args, **options) if output: if self.output_transaction: connection = connections[options.get('database', DEFAULT_DB_ALIAS)] output = '%s\n%s\n%s' % ( self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()), output, self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()), ) self.stdout.write(output) return output
查看Command
類的handle
函數, Command
重寫了父類的handle
方法
def handle(self, *args, **options): if not settings.DEBUG and not settings.ALLOWED_HOSTS: raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.') self.use_ipv6 = options['use_ipv6'] if self.use_ipv6 and not socket.has_ipv6: raise CommandError('Your Python does not support IPv6.') self._raw_ipv6 = False if not options['addrport']: self.addr = '' self.port = self.default_port else: m = re.match(naiveip_re, options['addrport']) if m is None: raise CommandError('"%s" is not a valid port number ' 'or address:port pair.' % options['addrport']) self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups() if not self.port.isdigit(): raise CommandError("%r is not a valid port number." % self.port) if self.addr: if _ipv6: self.addr = self.addr[1:-1] self.use_ipv6 = True self._raw_ipv6 = True elif self.use_ipv6 and not _fqdn: raise CommandError('"%s" is not a valid IPv6 address.' % self.addr) if not self.addr: self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr self._raw_ipv6 = self.use_ipv6 self.run(**options) def run(self, **options): """Run the server, using the autoreloader if needed.""" use_reloader = options['use_reloader'] if use_reloader: autoreload.run_with_reloader(self.inner_run, **options) else: self.inner_run(None, **options) def inner_run(self, *args, **options): # If an exception was silenced in ManagementUtility.execute in order # to be raised in the child process, raise it now. autoreload.raise_last_exception() threading = options['use_threading'] # 'shutdown_message' is a stealth option. shutdown_message = options.get('shutdown_message', '') quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' self.stdout.write("Performing system checks...\n\n") self.check(display_num_errors=True) # Need to check migrations here, so can't use the # requires_migrations_check attribute. self.check_migrations() now = datetime.now().strftime('%B %d, %Y - %X') self.stdout.write(now) self.stdout.write(( "Django version %(version)s, using settings %(settings)r\n" "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n" "Quit the server with %(quit_command)s.\n" ) % { "version": self.get_version(), "settings": settings.SETTINGS_MODULE, "protocol": self.protocol, "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, "port": self.port, "quit_command": quit_command, }) try: handler = self.get_handler(*args, **options) run(self.addr, int(self.port), handler, ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls) except socket.error as e: # Use helpful error messages instead of ugly tracebacks. ERRORS = { errno.EACCES: "You don't have permission to access that port.", errno.EADDRINUSE: "That port is already in use.", errno.EADDRNOTAVAIL: "That IP address can't be assigned to.", } try: error_text = ERRORS[e.errno] except KeyError: error_text = e self.stderr.write("Error: %s" % error_text) # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: if shutdown_message: self.stdout.write(shutdown_message) sys.exit(0)
最終經過WSGIServer
類啓動線程來處理http
請求
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer): server_address = (addr, port) if threading: httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {}) else: httpd_cls = server_cls httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) if threading: # ThreadingMixIn.daemon_threads indicates how threads will behave on an # abrupt shutdown; like quitting the server by the user or restarting # by the auto-reloader. True means the server will not wait for thread # termination before it quits. This will make auto-reloader faster # and will prevent the need to kill the server manually if a thread # isn't terminating correctly. httpd.daemon_threads = True httpd.set_app(wsgi_handler) httpd.serve_forever()
runserver
方式以command
的方式啓動wsgi
. 先初始化apps
和settings
對象;接着得到全部command
對象;懶加載調用setting._setup()
方法安裝配置;接着解析獲得子命令,調用django.setup()
配置logging
和初始化app_config
,導入models
;根據子命令獲取command
對象並將參數傳遞給其並執行,其餘參數根據management\commands
中的command
文件的定義進行解析,而後調用handle
方法進行執行;而後調用run
方法啓動wsgi
.