From d4cb729c9360572cddeda13720f1cde832ac1adc Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Thu, 7 Feb 2019 19:13:39 +0300 Subject: [PATCH] rework settings, add loading of the django.conf.global_settings, cleanups --- README.md | 8 +- django-stubs/conf/global_settings.pyi | 331 ++++++------------ django-stubs/contrib/auth/models.pyi | 2 +- django-stubs/shortcuts.pyi | 2 +- django-stubs/utils/translation/__init__.pyi | 1 + mypy_django_plugin/main.py | 39 ++- mypy_django_plugin/monkeypatch/__init__.py | 7 +- .../monkeypatch/dependencies.py | 64 +--- mypy_django_plugin/plugins/models.py | 11 +- mypy_django_plugin/plugins/settings.py | 43 +-- test-data/typecheck/settings.test | 33 +- 11 files changed, 180 insertions(+), 361 deletions(-) diff --git a/README.md b/README.md index f41802e..440d184 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,13 @@ plugins = in your `mypy.ini` file. -Also, it uses value of `DJANGO_SETTINGS_MODULE` from the environment, so set it before execution, otherwise some features will not work. + +### `django.conf.settings` support + +`settings.SETTING_NAME` will only work if `DJANGO_SETTINGS_MODULE` will be present in the environment, when mypy is executed. + +If some setting is not recognized to the plugin, but it's clearly there, try adding type annotation to it. + ## To get help diff --git a/django-stubs/conf/global_settings.pyi b/django-stubs/conf/global_settings.pyi index 46c2996..5a6011b 100644 --- a/django-stubs/conf/global_settings.pyi +++ b/django-stubs/conf/global_settings.pyi @@ -5,156 +5,67 @@ by the DJANGO_SETTINGS_MODULE environment variable. # This is defined here as a do-nothing function because we can't import # django.utils.translation -- that module depends on the settings. -def gettext_noop(s): - return s +from typing import Any, Dict, List, Optional, Pattern, Tuple, Protocol, Union, Callable, TYPE_CHECKING #################### # CORE # #################### +if TYPE_CHECKING: + from django.db.models.base import Model -DEBUG = False +DEBUG: bool = ... # Whether the framework should propagate raw exceptions rather than catching # them. This is useful under some testing situations and should never be used # on a live site. -DEBUG_PROPAGATE_EXCEPTIONS = False +DEBUG_PROPAGATE_EXCEPTIONS: bool = ... # People who get code error notifications. # In the format [('Full Name', 'email@example.com'), ('Full Name', 'anotheremail@example.com')] -ADMINS = [] +ADMINS: List[Tuple[str, str]] = ... # List of IP addresses, as strings, that: # * See debug comments, when DEBUG is true # * Receive x-headers -INTERNAL_IPS = [] +INTERNAL_IPS: List[str] = ... # Hosts/domain names that are valid for this site. # "*" matches anything, ".example.com" matches example.com and all subdomains -ALLOWED_HOSTS = [] +ALLOWED_HOSTS: List[str] = ... # Local time zone for this installation. All choices can be found here: # https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all # systems may support all possibilities). When USE_TZ is True, this is # interpreted as the default user time zone. -TIME_ZONE = "America/Chicago" +TIME_ZONE: str = ... # If you set this to True, Django will use timezone-aware datetimes. -USE_TZ = False +USE_TZ: bool = ... # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE: str = ... # Languages we provide translations for, out of the box. -LANGUAGES = [ - ("af", gettext_noop("Afrikaans")), - ("ar", gettext_noop("Arabic")), - ("ast", gettext_noop("Asturian")), - ("az", gettext_noop("Azerbaijani")), - ("bg", gettext_noop("Bulgarian")), - ("be", gettext_noop("Belarusian")), - ("bn", gettext_noop("Bengali")), - ("br", gettext_noop("Breton")), - ("bs", gettext_noop("Bosnian")), - ("ca", gettext_noop("Catalan")), - ("cs", gettext_noop("Czech")), - ("cy", gettext_noop("Welsh")), - ("da", gettext_noop("Danish")), - ("de", gettext_noop("German")), - ("dsb", gettext_noop("Lower Sorbian")), - ("el", gettext_noop("Greek")), - ("en", gettext_noop("English")), - ("en-au", gettext_noop("Australian English")), - ("en-gb", gettext_noop("British English")), - ("eo", gettext_noop("Esperanto")), - ("es", gettext_noop("Spanish")), - ("es-ar", gettext_noop("Argentinian Spanish")), - ("es-co", gettext_noop("Colombian Spanish")), - ("es-mx", gettext_noop("Mexican Spanish")), - ("es-ni", gettext_noop("Nicaraguan Spanish")), - ("es-ve", gettext_noop("Venezuelan Spanish")), - ("et", gettext_noop("Estonian")), - ("eu", gettext_noop("Basque")), - ("fa", gettext_noop("Persian")), - ("fi", gettext_noop("Finnish")), - ("fr", gettext_noop("French")), - ("fy", gettext_noop("Frisian")), - ("ga", gettext_noop("Irish")), - ("gd", gettext_noop("Scottish Gaelic")), - ("gl", gettext_noop("Galician")), - ("he", gettext_noop("Hebrew")), - ("hi", gettext_noop("Hindi")), - ("hr", gettext_noop("Croatian")), - ("hsb", gettext_noop("Upper Sorbian")), - ("hu", gettext_noop("Hungarian")), - ("ia", gettext_noop("Interlingua")), - ("id", gettext_noop("Indonesian")), - ("io", gettext_noop("Ido")), - ("is", gettext_noop("Icelandic")), - ("it", gettext_noop("Italian")), - ("ja", gettext_noop("Japanese")), - ("ka", gettext_noop("Georgian")), - ("kab", gettext_noop("Kabyle")), - ("kk", gettext_noop("Kazakh")), - ("km", gettext_noop("Khmer")), - ("kn", gettext_noop("Kannada")), - ("ko", gettext_noop("Korean")), - ("lb", gettext_noop("Luxembourgish")), - ("lt", gettext_noop("Lithuanian")), - ("lv", gettext_noop("Latvian")), - ("mk", gettext_noop("Macedonian")), - ("ml", gettext_noop("Malayalam")), - ("mn", gettext_noop("Mongolian")), - ("mr", gettext_noop("Marathi")), - ("my", gettext_noop("Burmese")), - ("nb", gettext_noop("Norwegian Bokmål")), - ("ne", gettext_noop("Nepali")), - ("nl", gettext_noop("Dutch")), - ("nn", gettext_noop("Norwegian Nynorsk")), - ("os", gettext_noop("Ossetic")), - ("pa", gettext_noop("Punjabi")), - ("pl", gettext_noop("Polish")), - ("pt", gettext_noop("Portuguese")), - ("pt-br", gettext_noop("Brazilian Portuguese")), - ("ro", gettext_noop("Romanian")), - ("ru", gettext_noop("Russian")), - ("sk", gettext_noop("Slovak")), - ("sl", gettext_noop("Slovenian")), - ("sq", gettext_noop("Albanian")), - ("sr", gettext_noop("Serbian")), - ("sr-latn", gettext_noop("Serbian Latin")), - ("sv", gettext_noop("Swedish")), - ("sw", gettext_noop("Swahili")), - ("ta", gettext_noop("Tamil")), - ("te", gettext_noop("Telugu")), - ("th", gettext_noop("Thai")), - ("tr", gettext_noop("Turkish")), - ("tt", gettext_noop("Tatar")), - ("udm", gettext_noop("Udmurt")), - ("uk", gettext_noop("Ukrainian")), - ("ur", gettext_noop("Urdu")), - ("vi", gettext_noop("Vietnamese")), - ("zh-hans", gettext_noop("Simplified Chinese")), - ("zh-hant", gettext_noop("Traditional Chinese")), -] +LANGUAGES: List[Tuple[str, str]] = ... # Languages using BiDi (right-to-left) layout -LANGUAGES_BIDI = ["he", "ar", "fa", "ur"] +LANGUAGES_BIDI: List[str] = ... # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. -USE_I18N = True -LOCALE_PATHS = [] +USE_I18N: bool = ... +LOCALE_PATHS: List[str] = ... # Settings for language cookie -LANGUAGE_COOKIE_NAME = "django_language" -LANGUAGE_COOKIE_AGE = None -LANGUAGE_COOKIE_DOMAIN = None -LANGUAGE_COOKIE_PATH = "/" +LANGUAGE_COOKIE_NAME: str = ... +LANGUAGE_COOKIE_AGE: Optional[int] = ... +LANGUAGE_COOKIE_DOMAIN: Optional[str] = ... +LANGUAGE_COOKIE_PATH: str = ... # If you set this to True, Django will format dates, numbers and calendars # according to user current locale. -USE_L10N = False +USE_L10N: bool = ... # Not-necessarily-technical managers of the site. They get broken link # notifications and other various emails. @@ -163,66 +74,69 @@ MANAGERS = ADMINS # Default content type and charset to use for all HttpResponse objects, if a # MIME type isn't manually specified. These are used to construct the # Content-Type header. -DEFAULT_CONTENT_TYPE = "text/html" -DEFAULT_CHARSET = "utf-8" +DEFAULT_CONTENT_TYPE: str = ... +DEFAULT_CHARSET: str = ... # Encoding of files read from disk (template and initial SQL files). -FILE_CHARSET = "utf-8" +FILE_CHARSET: str = ... # Email address that error messages come from. -SERVER_EMAIL = "root@localhost" +SERVER_EMAIL: str = ... # Database connection info. If left empty, will default to the dummy backend. -DATABASES = {} +DATABASES: Dict[str, Dict[str, Any]] = ... # Classes used to implement DB routing behavior. -DATABASE_ROUTERS = [] +class Router(Protocol): + def allow_migrate(self, db, app_label, **hints): ... + +DATABASE_ROUTERS: List[Union[str, Router]] = ... # The email backend to use. For possible shortcuts see django.core.mail. # The default is to use the SMTP backend. # Third-party backends can be specified by providing a Python path # to a module that defines an EmailBackend class. -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_BACKEND: str = ... # Host for sending email. -EMAIL_HOST = "localhost" +EMAIL_HOST: str = ... # Port for sending email. -EMAIL_PORT = 25 +EMAIL_PORT: int = ... # Whether to send SMTP 'Date' header in the local time zone or in UTC. -EMAIL_USE_LOCALTIME = False +EMAIL_USE_LOCALTIME: bool = ... # Optional SMTP authentication information for EMAIL_HOST. -EMAIL_HOST_USER = "" -EMAIL_HOST_PASSWORD = "" -EMAIL_USE_TLS = False -EMAIL_USE_SSL = False -EMAIL_SSL_CERTFILE = None -EMAIL_SSL_KEYFILE = None -EMAIL_TIMEOUT = None +EMAIL_HOST_USER: str = ... +EMAIL_HOST_PASSWORD: str = ... +EMAIL_USE_TLS: bool = ... +EMAIL_USE_SSL: bool = ... +EMAIL_SSL_CERTFILE: Optional[str] = ... +EMAIL_SSL_KEYFILE: Optional[str] = ... +EMAIL_TIMEOUT: Optional[int] = ... # List of strings representing installed apps. -INSTALLED_APPS = [] +INSTALLED_APPS: List[str] = ... -TEMPLATES = [] +TEMPLATES: List[Dict[str, Any]] = ... # Default form rendering class. -FORM_RENDERER = "django.forms.renderers.DjangoTemplates" +FORM_RENDERER: str = ... # Default email address to use for various automated correspondence from # the site managers. -DEFAULT_FROM_EMAIL = "webmaster@localhost" +DEFAULT_FROM_EMAIL: str = ... # Subject-line prefix for email messages send with django.core.mail.mail_admins # or ...mail_managers. Make sure to include the trailing space. -EMAIL_SUBJECT_PREFIX = "[Django] " +EMAIL_SUBJECT_PREFIX: str = ... # Whether to append trailing slashes to URLs. -APPEND_SLASH = True +APPEND_SLASH: bool = ... # Whether to prepend the "www." subdomain to URLs that don't have it. -PREPEND_WWW = False +PREPEND_WWW: bool = ... # Override the server-derived value of SCRIPT_NAME FORCE_SCRIPT_NAME = None @@ -237,9 +151,9 @@ FORCE_SCRIPT_NAME = None # re.compile(r'^SiteSucker.*'), # re.compile(r'^sohu-search'), # ] -DISALLOWED_USER_AGENTS = [] +DISALLOWED_USER_AGENTS: List[Pattern] = ... -ABSOLUTE_URL_OVERRIDES = {} +ABSOLUTE_URL_OVERRIDES: Dict[str, Callable[[Model], str]] = ... # List of compiled regular expression objects representing URLs that need not # be reported by BrokenLinkEmailsMiddleware. Here are a few examples: @@ -251,54 +165,51 @@ ABSOLUTE_URL_OVERRIDES = {} # re.compile(r'^/phpmyadmin/'), # re.compile(r'\.(cgi|php|pl)$'), # ] -IGNORABLE_404_URLS = [] +IGNORABLE_404_URLS: List[Pattern] = ... # A secret key for this particular Django installation. Used in secret-key # hashing algorithms. Set this in your settings, or Django will complain # loudly. -SECRET_KEY = "" +SECRET_KEY: str = ... # Default file storage mechanism that holds media. -DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" +DEFAULT_FILE_STORAGE: str = ... # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/var/www/example.com/media/" -MEDIA_ROOT = "" +MEDIA_ROOT: str = ... # URL that handles the media served from MEDIA_ROOT. # Examples: "http://example.com/media/", "http://media.example.com/" -MEDIA_URL = "" +MEDIA_URL: str = ... # Absolute path to the directory static files should be collected to. # Example: "/var/www/example.com/static/" -STATIC_ROOT = None +STATIC_ROOT: Optional[str] = ... # URL that handles the static files served from STATIC_ROOT. # Example: "http://example.com/static/", "http://static.example.com/" -STATIC_URL = None +STATIC_URL: Optional[str] = ... # List of upload handler classes to be applied in order. -FILE_UPLOAD_HANDLERS = [ - "django.core.files.uploadhandler.MemoryFileUploadHandler", - "django.core.files.uploadhandler.TemporaryFileUploadHandler", -] +FILE_UPLOAD_HANDLERS: List[str] = ... # Maximum size, in bytes, of a request before it will be streamed to the # file system instead of into memory. -FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB +FILE_UPLOAD_MAX_MEMORY_SIZE: int = ... # i.e. 2.5 MB # Maximum size in bytes of request data (excluding file uploads) that will be # read before a SuspiciousOperation (RequestDataTooBig) is raised. -DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB +DATA_UPLOAD_MAX_MEMORY_SIZE: int = ... # i.e. 2.5 MB # Maximum number of GET/POST parameters that will be read before a # SuspiciousOperation (TooManyFieldsSent) is raised. -DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000 +DATA_UPLOAD_MAX_NUMBER_FIELDS: int = ... # Directory in which upload streamed files will be temporarily saved. A value of # `None` will make Django use the operating system's default temporary directory # (i.e. "/tmp" on *nix systems). -FILE_UPLOAD_TEMP_DIR = None +FILE_UPLOAD_TEMP_DIR: Optional[str] = ... # The numeric mode to set newly-uploaded files to. The value should be a mode # you'd pass directly to os.chmod; see https://docs.python.org/library/os.html#files-and-directories. @@ -313,116 +224,91 @@ FILE_UPLOAD_DIRECTORY_PERMISSIONS = None # The directory where this setting is pointing should contain subdirectories # named as the locales, containing a formats.py file # (i.e. "myproject.locale" for myproject/locale/en/formats.py etc. use) -FORMAT_MODULE_PATH = None +FORMAT_MODULE_PATH: Optional[str] = ... # Default formatting for date objects. See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -DATE_FORMAT = "N j, Y" +DATE_FORMAT: str = ... # Default formatting for datetime objects. See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -DATETIME_FORMAT = "N j, Y, P" +DATETIME_FORMAT: str = ... # Default formatting for time objects. See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -TIME_FORMAT = "P" +TIME_FORMAT: str = ... # Default formatting for date objects when only the year and month are relevant. # See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -YEAR_MONTH_FORMAT = "F Y" +YEAR_MONTH_FORMAT: str = ... # Default formatting for date objects when only the month and day are relevant. # See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -MONTH_DAY_FORMAT = "F j" +MONTH_DAY_FORMAT: str = ... # Default short formatting for date objects. See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -SHORT_DATE_FORMAT = "m/d/Y" +SHORT_DATE_FORMAT: str = ... # Default short formatting for datetime objects. # See all available format strings here: # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -SHORT_DATETIME_FORMAT = "m/d/Y P" +SHORT_DATETIME_FORMAT: str = ... # Default formats to be used when parsing dates from input boxes, in order # See all available format string here: # https://docs.python.org/library/datetime.html#strftime-behavior # * Note that these format strings are different from the ones to display dates -DATE_INPUT_FORMATS = [ - "%Y-%m-%d", - "%m/%d/%Y", - "%m/%d/%y", # '2006-10-25', '10/25/2006', '10/25/06' - "%b %d %Y", - "%b %d, %Y", # 'Oct 25 2006', 'Oct 25, 2006' - "%d %b %Y", - "%d %b, %Y", # '25 Oct 2006', '25 Oct, 2006' - "%B %d %Y", - "%B %d, %Y", # 'October 25 2006', 'October 25, 2006' - "%d %B %Y", - "%d %B, %Y", # '25 October 2006', '25 October, 2006' -] +DATE_INPUT_FORMATS: List[str] = ... # Default formats to be used when parsing times from input boxes, in order # See all available format string here: # https://docs.python.org/library/datetime.html#strftime-behavior # * Note that these format strings are different from the ones to display dates -TIME_INPUT_FORMATS = ["%H:%M:%S", "%H:%M:%S.%f", "%H:%M"] # '14:30:59' # '14:30:59.000200' # '14:30' +TIME_INPUT_FORMATS: List[str] = ... # '14:30:59' # '14:30:59.000200' # '14:30' # Default formats to be used when parsing dates and times from input boxes, # in order # See all available format string here: # https://docs.python.org/library/datetime.html#strftime-behavior # * Note that these format strings are different from the ones to display dates -DATETIME_INPUT_FORMATS = [ - "%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59' - "%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200' - "%Y-%m-%d %H:%M", # '2006-10-25 14:30' - "%Y-%m-%d", # '2006-10-25' - "%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59' - "%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200' - "%m/%d/%Y %H:%M", # '10/25/2006 14:30' - "%m/%d/%Y", # '10/25/2006' - "%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59' - "%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200' - "%m/%d/%y %H:%M", # '10/25/06 14:30' - "%m/%d/%y", # '10/25/06' -] +DATETIME_INPUT_FORMATS: List[str] = ... # First day of week, to be used on calendars # 0 means Sunday, 1 means Monday... -FIRST_DAY_OF_WEEK = 0 +FIRST_DAY_OF_WEEK: int = ... # Decimal separator symbol -DECIMAL_SEPARATOR = "." +DECIMAL_SEPARATOR: str = ... # Boolean that sets whether to add thousand separator when formatting numbers -USE_THOUSAND_SEPARATOR = False +USE_THOUSAND_SEPARATOR: bool = ... # Number of digits that will be together, when splitting them by # THOUSAND_SEPARATOR. 0 means no grouping, 3 means splitting by thousands... -NUMBER_GROUPING = 0 +NUMBER_GROUPING: int = ... # Thousand separator symbol -THOUSAND_SEPARATOR = "," +THOUSAND_SEPARATOR: str = ... # The tablespaces to use for each model when not specified otherwise. -DEFAULT_TABLESPACE = "" -DEFAULT_INDEX_TABLESPACE = "" +DEFAULT_TABLESPACE: str = ... +DEFAULT_INDEX_TABLESPACE: str = ... # Default X-Frame-Options header value -X_FRAME_OPTIONS = "SAMEORIGIN" +X_FRAME_OPTIONS: str = ... -USE_X_FORWARDED_HOST = False -USE_X_FORWARDED_PORT = False +USE_X_FORWARDED_HOST: bool = ... +USE_X_FORWARDED_PORT: bool = ... # The Python dotted path to the WSGI application that Django's internal server # (runserver) will use. If `None`, the return value of # 'django.core.wsgi.get_wsgi_application' is used, thus preserving the same # behavior as previous versions of Django. Otherwise this should point to an # actual WSGI application object. -WSGI_APPLICATION = None +WSGI_APPLICATION: Optional[str] = ... # If your Django app is behind a proxy that sets a header to specify secure # connections, AND that proxy ensures that user-submitted headers with the @@ -431,7 +317,7 @@ WSGI_APPLICATION = None # that header/value, request.is_secure() will return True. # WARNING! Only set this if you fully understand what you're doing. Otherwise, # you may be opening yourself up to a security risk. -SECURE_PROXY_SSL_HEADER = None +SECURE_PROXY_SSL_HEADER: Optional[Tuple[str, str]] = ... ############## # MIDDLEWARE # @@ -440,7 +326,7 @@ SECURE_PROXY_SSL_HEADER = None # List of middleware to use. Order is important; in the request phase, these # middleware will be applied in the order given, and in the response # phase the middleware will be applied in reverse order. -MIDDLEWARE = [] +MIDDLEWARE: List[str] = ... ############ # SESSIONS # @@ -453,7 +339,7 @@ SESSION_COOKIE_NAME = "sessionid" # Age of cookie, in seconds (default: 2 weeks). SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # A string like "example.com", or None for standard domain cookie. -SESSION_COOKIE_DOMAIN = None +SESSION_COOKIE_DOMAIN: Optional[str] = ... # Whether the session cookie should be secure (https:// only). SESSION_COOKIE_SECURE = False # The path of the session cookie. @@ -471,7 +357,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_ENGINE = "django.contrib.sessions.backends.db" # Directory to store session files if using the file session module. If None, # the backend will use a sensible default. -SESSION_FILE_PATH = None +SESSION_FILE_PATH: Optional[str] = ... # class to serialize session data SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" @@ -480,7 +366,7 @@ SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" ######### # The cache backends to use. -CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} +CACHES: Dict[str, Dict[str, Any]] = ... CACHE_MIDDLEWARE_KEY_PREFIX = "" CACHE_MIDDLEWARE_SECONDS = 600 CACHE_MIDDLEWARE_ALIAS = "default" @@ -489,15 +375,15 @@ CACHE_MIDDLEWARE_ALIAS = "default" # AUTHENTICATION # ################## -AUTH_USER_MODEL = "auth.User" +AUTH_USER_MODEL: str = ... -AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] +AUTHENTICATION_BACKENDS: List[str] = ... LOGIN_URL = "/accounts/login/" -LOGIN_REDIRECT_URL = "/accounts/profile/" +LOGIN_REDIRECT_URL: str = ... -LOGOUT_REDIRECT_URL = None +LOGOUT_REDIRECT_URL: Optional[str] = ... # The number of days a password reset link is valid for PASSWORD_RESET_TIMEOUT_DAYS = 3 @@ -505,14 +391,9 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3 # the first hasher in this list is the preferred algorithm. any # password using different algorithms will be converted automatically # upon login -PASSWORD_HASHERS = [ - "django.contrib.auth.hashers.PBKDF2PasswordHasher", - "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", - "django.contrib.auth.hashers.Argon2PasswordHasher", - "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", -] +PASSWORD_HASHERS: List[str] = ... -AUTH_PASSWORD_VALIDATORS = [] +AUTH_PASSWORD_VALIDATORS: List[Dict[str, str]] = ... ########### # SIGNING # @@ -537,7 +418,7 @@ CSRF_COOKIE_SECURE = False CSRF_COOKIE_HTTPONLY = False CSRF_COOKIE_SAMESITE = "Lax" CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN" -CSRF_TRUSTED_ORIGINS = [] +CSRF_TRUSTED_ORIGINS: List[str] = ... CSRF_USE_SESSIONS = False ############ @@ -558,7 +439,7 @@ MESSAGE_STORAGE = "django.contrib.messages.storage.fallback.FallbackStorage" LOGGING_CONFIG = "logging.config.dictConfig" # Custom logging configuration. -LOGGING = {} +LOGGING: Dict[str, Any] = ... # Default exception reporter filter class used in case none has been # specifically assigned to the HttpRequest instance. @@ -573,39 +454,35 @@ TEST_RUNNER = "django.test.runner.DiscoverRunner" # Apps that don't need to be serialized at test database creation time # (only apps with migrations are to start with) -TEST_NON_SERIALIZED_APPS = [] +TEST_NON_SERIALIZED_APPS: List[str] = ... ############ # FIXTURES # ############ # The list of directories to search for fixtures -FIXTURE_DIRS = [] +FIXTURE_DIRS: List[str] = ... ############### # STATICFILES # ############### # A list of locations of additional static files -STATICFILES_DIRS = [] +STATICFILES_DIRS: List[str] = ... # The default file storage backend used during the build process -STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" +STATICFILES_STORAGE: str = ... # List of finder classes that know how to find static files in # various locations. -STATICFILES_FINDERS = [ - "django.contrib.staticfiles.finders.FileSystemFinder", - "django.contrib.staticfiles.finders.AppDirectoriesFinder", - # 'django.contrib.staticfiles.finders.DefaultStorageFinder', -] +STATICFILES_FINDERS: List[str] = ... ############## # MIGRATIONS # ############## # Migration module overrides for apps, by app label. -MIGRATION_MODULES = {} +MIGRATION_MODULES: Dict[str, str] = ... ################# # SYSTEM CHECKS # @@ -615,7 +492,7 @@ MIGRATION_MODULES = {} # issues like warnings, infos or debugs will not generate a message. Silencing # serious issues like errors and criticals does not result in hiding the # message, but Django will not stop you from e.g. running server. -SILENCED_SYSTEM_CHECKS = [] +SILENCED_SYSTEM_CHECKS: List[str] = ... ####################### # SECURITY MIDDLEWARE # @@ -625,6 +502,6 @@ SECURE_CONTENT_TYPE_NOSNIFF = False SECURE_HSTS_INCLUDE_SUBDOMAINS = False SECURE_HSTS_PRELOAD = False SECURE_HSTS_SECONDS = 0 -SECURE_REDIRECT_EXEMPT = [] +SECURE_REDIRECT_EXEMPT: List[str] = ... SECURE_SSL_HOST = None SECURE_SSL_REDIRECT = False diff --git a/django-stubs/contrib/auth/models.pyi b/django-stubs/contrib/auth/models.pyi index 15e7b31..e7e3995 100644 --- a/django-stubs/contrib/auth/models.pyi +++ b/django-stubs/contrib/auth/models.pyi @@ -58,7 +58,7 @@ class PermissionsMixin(models.Model): def has_perms(self, perm_list: Union[List[str], Set[str], Tuple[str]], obj: None = ...) -> bool: ... def has_module_perms(self, app_label: str) -> bool: ... -class AbstractUser(AbstractBaseUser, PermissionsMixin): +class AbstractUser(AbstractBaseUser, PermissionsMixin): # type: ignore is_superuser: bool username_validator: Any = ... username: str = ... diff --git a/django-stubs/shortcuts.pyi b/django-stubs/shortcuts.pyi index 8892583..f245745 100644 --- a/django-stubs/shortcuts.pyi +++ b/django-stubs/shortcuts.pyi @@ -2,7 +2,7 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union, Sequence, P from django.db.models import Manager, QuerySet from django.db.models.base import Model -from django.http.response import HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponse as HttpResponse, HttpResponseRedirect as HttpResponseRedirect from django.http import HttpRequest diff --git a/django-stubs/utils/translation/__init__.pyi b/django-stubs/utils/translation/__init__.pyi index 564935d..cc514d5 100644 --- a/django-stubs/utils/translation/__init__.pyi +++ b/django-stubs/utils/translation/__init__.pyi @@ -61,6 +61,7 @@ class override(ContextDecorator): def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: ... def get_language() -> Optional[str]: ... +def get_language_from_path(path: str) -> Optional[str]: ... def get_language_bidi() -> bool: ... def check_for_language(lang_code: Optional[str]) -> bool: ... def to_language(locale: str) -> str: ... diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 7952505..13299ce 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,18 +1,18 @@ import os -from typing import Callable, Optional, cast, Dict +from typing import Callable, Dict, Optional, cast from mypy.checker import TypeChecker from mypy.nodes import TypeInfo from mypy.options import Options -from mypy.plugin import Plugin, FunctionContext, ClassDefContext, MethodContext -from mypy.types import Type, Instance +from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin +from mypy.types import Instance, Type from mypy_django_plugin import helpers, monkeypatch from mypy_django_plugin.plugins.fields import determine_type_of_array_field from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations from mypy_django_plugin.plugins.models import process_model_class from mypy_django_plugin.plugins.related_fields import extract_to_parameter_as_get_ret_type_for_related_field, reparametrize_with -from mypy_django_plugin.plugins.settings import DjangoConfSettingsInitializerHook +from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject def transform_model_class(ctx: ClassDefContext) -> None: @@ -55,19 +55,21 @@ def determine_proper_manager_type(ctx: FunctionContext) -> Type: class DjangoPlugin(Plugin): - def __init__(self, - options: Options) -> None: + def __init__(self, options: Options) -> None: super().__init__(options) + monkeypatch.restore_original_load_graph() + monkeypatch.restore_original_dependencies_handling() + + settings_modules = ['django.conf.global_settings'] self.django_settings = os.environ.get('DJANGO_SETTINGS_MODULE') if self.django_settings: - monkeypatch.load_graph_to_add_settings_file_as_a_source_seed(self.django_settings) - monkeypatch.inject_dependencies(self.django_settings) - else: - monkeypatch.restore_original_load_graph() - monkeypatch.restore_original_dependencies_handling() + settings_modules.append(self.django_settings) - def get_current_model_bases(self) -> Dict[str, int]: + monkeypatch.add_modules_as_a_source_seed_files(settings_modules) + monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules) + + def _get_current_model_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): if 'django' not in model_sym.node.metadata: @@ -78,7 +80,7 @@ class DjangoPlugin(Plugin): else: return {} - def get_current_manager_bases(self) -> Dict[str, int]: + def _get_current_manager_bases(self) -> Dict[str, int]: manager_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME) if manager_sym is not None and isinstance(manager_sym.node, TypeInfo): if 'django' not in manager_sym.node.metadata: @@ -99,7 +101,7 @@ class DjangoPlugin(Plugin): if fullname == 'django.contrib.postgres.fields.array.ArrayField': return determine_type_of_array_field - manager_bases = self.get_current_manager_bases() + manager_bases = self._get_current_manager_bases() if fullname in manager_bases: return determine_proper_manager_type @@ -112,13 +114,16 @@ class DjangoPlugin(Plugin): def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - if fullname in self.get_current_model_bases(): + if fullname in self._get_current_model_bases(): return transform_model_class if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS: - return DjangoConfSettingsInitializerHook(settings_module=self.django_settings) + settings_modules = ['django.conf.global_settings'] + if self.django_settings: + settings_modules.append(self.django_settings) + return AddSettingValuesToDjangoConfObject(settings_modules) - if fullname in self.get_current_manager_bases(): + if fullname in self._get_current_manager_bases(): return transform_manager_class return None diff --git a/mypy_django_plugin/monkeypatch/__init__.py b/mypy_django_plugin/monkeypatch/__init__.py index cd6e475..7404c26 100644 --- a/mypy_django_plugin/monkeypatch/__init__.py +++ b/mypy_django_plugin/monkeypatch/__init__.py @@ -1,5 +1,4 @@ -from .dependencies import (load_graph_to_add_settings_file_as_a_source_seed, - inject_dependencies, +from .dependencies import (add_modules_as_a_source_seed_files, + inject_modules_as_dependencies_for_django_conf_settings, restore_original_load_graph, - restore_original_dependencies_handling, - process_settings_before_dependants) + restore_original_dependencies_handling) diff --git a/mypy_django_plugin/monkeypatch/dependencies.py b/mypy_django_plugin/monkeypatch/dependencies.py index 3a169d6..45252c3 100644 --- a/mypy_django_plugin/monkeypatch/dependencies.py +++ b/mypy_django_plugin/monkeypatch/dependencies.py @@ -1,26 +1,25 @@ -from typing import List, Optional, AbstractSet, MutableSet, Set +from typing import List, Optional -from mypy.build import BuildManager, Graph, State, PRI_ALL +from mypy import build +from mypy.build import BuildManager, Graph, State from mypy.modulefinder import BuildSource +old_load_graph = build.load_graph +OldState = build.State + def is_module_present_in_sources(module_name: str, sources: List[BuildSource]): return any([source.module == module_name for source in sources]) -from mypy import build - -old_load_graph = build.load_graph -OldState = build.State -old_sorted_components = build.sorted_components - - -def load_graph_to_add_settings_file_as_a_source_seed(settings_module: str): +def add_modules_as_a_source_seed_files(modules: List[str]) -> None: def patched_load_graph(sources: List[BuildSource], manager: BuildManager, old_graph: Optional[Graph] = None, new_modules: Optional[List[State]] = None): - if not is_module_present_in_sources(settings_module, sources): - sources.append(BuildSource(None, settings_module, None)) + # add global settings + for module_name in modules: + if not is_module_present_in_sources(module_name, sources): + sources.append(BuildSource(None, module_name, None)) return old_load_graph(sources=sources, manager=manager, old_graph=old_graph, @@ -35,14 +34,14 @@ def restore_original_load_graph(): build.load_graph = old_load_graph -def inject_dependencies(settings_module: str): +def inject_modules_as_dependencies_for_django_conf_settings(modules: List[str]) -> None: from mypy import build class PatchedState(build.State): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.id == 'django.conf': - self.dependencies.append(settings_module) + self.dependencies.extend(modules) build.State = PatchedState @@ -51,40 +50,3 @@ def restore_original_dependencies_handling(): from mypy import build build.State = OldState - - -def _extract_dependencies(graph: Graph, state_id: str, visited_modules: Set[str]) -> Set[str]: - visited_modules.add(state_id) - dependencies = set(graph[state_id].dependencies) - for new_dep_id in dependencies.copy(): - if new_dep_id not in visited_modules: - dependencies.update(_extract_dependencies(graph, new_dep_id, visited_modules)) - return dependencies - - -def extract_module_dependencies(graph: Graph, state_id: str) -> Set[str]: - visited_modules = set() - return _extract_dependencies(graph, state_id, visited_modules=visited_modules) - - -def process_settings_before_dependants(settings_module: str): - def patched_sorted_components(graph: Graph, - vertices: Optional[AbstractSet[str]] = None, - pri_max: int = PRI_ALL) -> List[AbstractSet[str]]: - sccs = old_sorted_components(graph, - vertices=vertices, - pri_max=pri_max) - for i, scc in enumerate(sccs.copy()): - if 'django.conf' in scc: - django_conf_deps = set(extract_module_dependencies(graph, 'django.conf')).union({'django.conf'}) - old_scc_modified = scc.difference(django_conf_deps) - new_scc = scc.difference(old_scc_modified) - if not old_scc_modified: - # already processed - break - sccs[i] = frozenset(old_scc_modified) - sccs.insert(i, frozenset(new_scc)) - break - return sccs - - build.sorted_components = patched_sorted_components diff --git a/mypy_django_plugin/plugins/models.py b/mypy_django_plugin/plugins/models.py index 4ab4597..dbd3344 100644 --- a/mypy_django_plugin/plugins/models.py +++ b/mypy_django_plugin/plugins/models.py @@ -1,9 +1,9 @@ -import dataclasses -from abc import abstractmethod, ABCMeta -from typing import cast, Iterator, Tuple, Optional, Dict +from abc import ABCMeta, abstractmethod +from typing import Dict, Iterator, Optional, Tuple, cast -from mypy.nodes import ClassDef, AssignmentStmt, CallExpr, MemberExpr, StrExpr, NameExpr, MDEF, TypeInfo, Var, SymbolTableNode, \ - Lvalue, Expression, MypyFile, Context +import dataclasses +from mypy.nodes import AssignmentStmt, CallExpr, ClassDef, Context, Expression, Lvalue, MDEF, MemberExpr, MypyFile, NameExpr, \ + StrExpr, SymbolTableNode, TypeInfo, Var from mypy.plugin import ClassDefContext from mypy.semanal import SemanticAnalyzerPass2 from mypy.types import Instance @@ -45,6 +45,7 @@ class ModelClassInitializer(metaclass=ABCMeta): var._fullname = self.model_classdef.info.fullname() + '.' + name var.is_inferred = True var.is_initialized_in_class = True + var.is_classvar = True self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var) @abstractmethod diff --git a/mypy_django_plugin/plugins/settings.py b/mypy_django_plugin/plugins/settings.py index a9d48c3..1f29871 100644 --- a/mypy_django_plugin/plugins/settings.py +++ b/mypy_django_plugin/plugins/settings.py @@ -1,9 +1,9 @@ -from typing import cast, List, Optional +from typing import List, Optional, cast -from mypy.nodes import Var, Context, SymbolNode, SymbolTableNode +from mypy.nodes import ClassDef, Context, MypyFile, SymbolNode, SymbolTableNode, Var from mypy.plugin import ClassDefContext from mypy.semanal import SemanticAnalyzerPass2 -from mypy.types import Instance, UnionType, NoneTyp, Type +from mypy.types import Instance, NoneTyp, Type, UnionType def get_error_context(node: SymbolNode) -> Context: @@ -36,39 +36,22 @@ def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]: return None -def add_settings_to_django_conf_object(ctx: ClassDefContext, - settings_module: str) -> None: - api = cast(SemanticAnalyzerPass2, ctx.api) - if settings_module not in api.modules: - return None - - settings_file = api.modules[settings_module] - for name, sym in settings_file.names.items(): +def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> None: + for name, sym in module.names.items(): if name.isupper() and isinstance(sym.node, Var): if sym.type is not None: copied = make_sym_copy_of_setting(sym) if copied is None: continue - ctx.cls.info.names[name] = copied - # else: - # TODO: figure out suggestion to add type annotation - # context = Context() - # module, node_name = sym.node.fullname().rsplit('.', 1) - # module_file = api.modules.get(module) - # if module_file is None: - # return None - # context.set_line(sym.node) - # api.msg.report(f"Need type annotation for '{sym.node.name()}'", context, - # severity='error', file=module_file.path) - ctx.cls.info.fallback_to_any = True + settings_classdef.info.names[name] = copied -class DjangoConfSettingsInitializerHook(object): - def __init__(self, settings_module: Optional[str]): - self.settings_module = settings_module +class AddSettingValuesToDjangoConfObject: + def __init__(self, settings_modules: List[str]): + self.settings_modules = settings_modules def __call__(self, ctx: ClassDefContext) -> None: - if not self.settings_module: - return - - add_settings_to_django_conf_object(ctx, self.settings_module) + api = cast(SemanticAnalyzerPass2, ctx.api) + for module_name in self.settings_modules: + module = api.modules[module_name] + load_settings_from_module(ctx.cls, module=module) diff --git a/test-data/typecheck/settings.test b/test-data/typecheck/settings.test index a58d3e0..31d2321 100644 --- a/test-data/typecheck/settings.test +++ b/test-data/typecheck/settings.test @@ -32,30 +32,13 @@ from pathlib import Path ROOT_DIR = Path(__file__) -[CASE test_circular_dependency_in_settings] +[CASE global_settings_are_always_loaded] from django.conf import settings -class Class: - pass -reveal_type(settings.REGISTRY) # E: Revealed type is 'Union[main.Class, None]' -reveal_type(settings.LIST) # E: Revealed type is 'Any' -reveal_type(settings.BASE_LIST) # E: Revealed type is 'Any' + +reveal_type(settings.AUTH_USER_MODEL) # E: Revealed type is 'builtins.str' +reveal_type(settings.AUTHENTICATION_BACKENDS) # E: Revealed type is 'builtins.list[builtins.str]' [out] -[env DJANGO_SETTINGS_MODULE=mysettings] -[file mysettings.py] -from typing import Optional -from base import * - -LIST = ['1', '2'] - -[file base.py] -from typing import TYPE_CHECKING, Optional -if TYPE_CHECKING: - from main import Class - -REGISTRY: Optional['Class'] = None -BASE_LIST = ['3', '4'] - [CASE test_circular_dependency_in_settings_works_if_settings_have_annotations] from django.conf import settings class Class: @@ -76,10 +59,12 @@ MYSETTING = 1122 REGISTRY: Optional['Class'] = None LIST: List[str] = ['1', '2'] -[CASE allow_calls_to_nonexistent_members_for_now] +[CASE fail_if_there_is_no_setting] from django.conf import settings -reveal_type(settings.NOT_EXISTING) # E: Revealed type is 'Any' +reveal_type(settings.NOT_EXISTING) [env DJANGO_SETTINGS_MODULE=mysettings] [file mysettings.py] -[out] \ No newline at end of file +[out] +main:2: error: Revealed type is 'Any' +main:2: error: "LazySettings" has no attribute "NOT_EXISTING" \ No newline at end of file