# https://github.com/django/asgiref/issues/179 from asgiref.sync import SyncToAsync old_init = SyncToAsync.__init__ def _thread_sensitive_init(self, func, thread_sensitive=True): return old_init(self, func, thread_sensitive=True) SyncToAsync.__init__ = _thread_sensitive_init import logging import django.conf import os import re import sys from sys import argv from pathlib import Path from typing import Optional # avoid confusion with otree_startup.settings from django.conf import settings as django_settings from importlib import import_module from django.core.management import get_commands, load_command_class import django from django.core.management.base import BaseCommand # "from .settings import ..." actually imports the whole settings module # confused me, it was overwriting django.conf.settings above # https://docs.python.org/3/reference/import.html#submodules from otree_startup.settings import augment_settings from otree import __version__ # REMEMBER TO ALSO UPDATE THE PROJECT TEMPLATE from otree_startup.settings import get_default_settings logger = logging.getLogger(__name__) MAIN_HELP_TEXT = ''' Type 'otree help ' for help on a specific subcommand. Available subcommands: browser_bots create_session devserver django_test resetdb prodserver prodserver1of2 prodserver2of2 shell startapp startproject test unzip zip zipserver ''' COMMAND_ALIASES = dict( test='bots', runprodserver='prodserver', webandworkers='prodserver1of2', runprodserver1of2='prodserver1of2', runprodserver2of2='prodserver2of2', timeoutworker='prodserver2of2', ) def execute_from_command_line(*args, **kwargs): ''' Top-level entry point. - figures out which subcommand is being run - sets up django & configures settings - runs the subcommand We have to ignore the args to this function. If the user runs "python manage.py [subcommand]", then argv is indeed passed, but if they run "otree [subcommand]", it executes the autogenerated console_scripts shim, which does not pass any args to this function, just: load_entry_point('otree', 'console_scripts', 'otree')() This is called if people use manage.py, or if people use the otree script. script_file is no longer used, but we need it for compat ''' if len(argv) == 1: # default command argv.append('help') subcmd = argv[1] subcmd = COMMAND_ALIASES.get(subcmd, subcmd) if subcmd == 'zipserver': from . import zipserver # expensive import zipserver.main(argv[2:]) # better to return than sys.exit because testing is complicated # with sys.exit -- if you mock it, then the function keeps executing. return if subcmd == 'devserver': from . import devserver # expensive import devserver.main(argv[2:]) # better to return than sys.exit because testing is complicated # with sys.exit -- if you mock it, then the function keeps executing. return # Add the current directory to sys.path so that Python can find # the settings module. # when using "python manage.py" this is not necessary because # the entry-point script's dir is automatically added to sys.path. # but the 'otree' command script is located outside of the project # directory. if os.getcwd() not in sys.path: sys.path.insert(0, os.getcwd()) # to match manage.py: # make it configurable so i can test it. # and it must be an env var, because # note: we will never get ImproperlyConfigured, # because that only happens when DJANGO_SETTINGS_MODULE is not set os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') DJANGO_SETTINGS_MODULE = os.environ['DJANGO_SETTINGS_MODULE'] if subcmd in ['help', '--help', '-h'] and len(argv) == 2: sys.stdout.write(MAIN_HELP_TEXT) return # need to set env var rather than setting otree.common.USE_TIMEOUT_WORKER because # that module cannot be loaded yet. # we no longer rely on redis, so eventually we should use the env var USE_TIMEOUT_WORKER. # but for now keep it while test out whether we can skip using redis if subcmd in ['prodserver', 'prodserver1of2']: os.environ['USE_TIMEOUT_WORKER'] = '1' if subcmd in [ 'startproject', 'version', '--version', 'compilemessages', 'makemessages', 'unzip', 'zip', ]: django_settings.configure(**get_default_settings({})) else: try: configure_settings(DJANGO_SETTINGS_MODULE) except ModuleNotFoundError as exc: if exc.name == DJANGO_SETTINGS_MODULE.split('.')[-1]: msg = ( "Cannot find oTree settings. " "Please 'cd' to your oTree project folder, " "which contains a settings.py file." ) logger.warning(msg) return raise do_django_setup() if subcmd == 'help' and len(argv) >= 3: about_cmd = argv[2] about_cmd = COMMAND_ALIASES.get(about_cmd, about_cmd) fetch_command(about_cmd).print_help('otree', about_cmd) elif subcmd in ("version", "--version"): sys.stdout.write(__version__ + '\n') else: fetch_command(subcmd).run_from_argv(argv) def configure_settings(DJANGO_SETTINGS_MODULE: str = 'settings'): user_settings_module = import_module(DJANGO_SETTINGS_MODULE) user_settings_dict = {} user_settings_dict['BASE_DIR'] = os.path.dirname( os.path.abspath(user_settings_module.__file__) ) # this is how Django reads settings from a settings module for setting_name in dir(user_settings_module): if setting_name.isupper(): setting_value = getattr(user_settings_module, setting_name) user_settings_dict[setting_name] = setting_value augment_settings(user_settings_dict) django_settings.configure(**user_settings_dict) def do_django_setup(): try: django.setup() except Exception as exc: # it would be nice to catch ModuleNotFoundError but need a good way # to differentiate between the app being in SESSION_CONFIGS vs # EXTENSION_APPS vs a regular import statement. import colorama colorama.init(autoreset=True) print_colored_traceback_and_exit(exc) def fetch_command(subcommand: str) -> BaseCommand: """ Tries 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. override a few django commands in the case where settings not loaded. hard to test this because we need to simulate settings not being configured """ if subcommand in ['startapp', 'startproject', 'unzip', 'zip']: command_module = import_module( 'otree.management.commands.{}'.format(subcommand) ) return command_module.Command() commands = get_commands() try: app_name = commands[subcommand] except KeyError: sys.stderr.write( "Unknown command: %r\nType 'otree help' for usage.\n" % subcommand ) 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 highlight(string): from termcolor import colored return colored(string, 'white', 'on_blue') def print_colored_traceback_and_exit(exc): import traceback import sys # before we used BASE_DIR but apparently that setting was not set yet # (not sure why) # so use os.getcwd() instead. # also, with BASE_DIR, I got "unknown command: devserver", as if # the list of commands was not loaded. current_dir = os.getcwd() frames = traceback.extract_tb(exc.__traceback__) new_frames = [] for frame in frames: filename, lineno, name, line = frame if current_dir in filename: filename = highlight(filename) line = highlight(line) new_frames.append([filename, lineno, name, line]) # taken from django source? lines = ['Traceback (most recent call last):\n'] lines += traceback.format_list(new_frames) final_lines = traceback.format_exception_only(type(exc), exc) # filename is only available for SyntaxError if isinstance(exc, SyntaxError) and current_dir in exc.filename: final_lines = [highlight(line) for line in final_lines] lines += final_lines for line in lines: sys.stdout.write(line) sys.exit(-1)