plain 0.1.0__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.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from io import IOBase
|
|
3
|
+
|
|
4
|
+
from plain import signals
|
|
5
|
+
from plain.http import HttpRequest, QueryDict, parse_cookie
|
|
6
|
+
from plain.internal.handlers import base
|
|
7
|
+
from plain.utils.encoding import repercent_broken_unicode
|
|
8
|
+
from plain.utils.functional import cached_property
|
|
9
|
+
from plain.utils.regex_helper import _lazy_re_compile
|
|
10
|
+
|
|
11
|
+
_slashes_re = _lazy_re_compile(rb"/+")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LimitedStream(IOBase):
|
|
15
|
+
"""
|
|
16
|
+
Wrap another stream to disallow reading it past a number of bytes.
|
|
17
|
+
|
|
18
|
+
Based on the implementation from werkzeug.wsgi.LimitedStream
|
|
19
|
+
See https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, stream, limit):
|
|
23
|
+
self._read = stream.read
|
|
24
|
+
self._readline = stream.readline
|
|
25
|
+
self._pos = 0
|
|
26
|
+
self.limit = limit
|
|
27
|
+
|
|
28
|
+
def read(self, size=-1, /):
|
|
29
|
+
_pos = self._pos
|
|
30
|
+
limit = self.limit
|
|
31
|
+
if _pos >= limit:
|
|
32
|
+
return b""
|
|
33
|
+
if size == -1 or size is None:
|
|
34
|
+
size = limit - _pos
|
|
35
|
+
else:
|
|
36
|
+
size = min(size, limit - _pos)
|
|
37
|
+
data = self._read(size)
|
|
38
|
+
self._pos += len(data)
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
def readline(self, size=-1, /):
|
|
42
|
+
_pos = self._pos
|
|
43
|
+
limit = self.limit
|
|
44
|
+
if _pos >= limit:
|
|
45
|
+
return b""
|
|
46
|
+
if size == -1 or size is None:
|
|
47
|
+
size = limit - _pos
|
|
48
|
+
else:
|
|
49
|
+
size = min(size, limit - _pos)
|
|
50
|
+
line = self._readline(size)
|
|
51
|
+
self._pos += len(line)
|
|
52
|
+
return line
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WSGIRequest(HttpRequest):
|
|
56
|
+
non_picklable_attrs = HttpRequest.non_picklable_attrs | frozenset(["environ"])
|
|
57
|
+
meta_non_picklable_attrs = frozenset(["wsgi.errors", "wsgi.input"])
|
|
58
|
+
|
|
59
|
+
def __init__(self, environ):
|
|
60
|
+
# A unique ID we can use to trace this request
|
|
61
|
+
self.unique_id = str(uuid.uuid4())
|
|
62
|
+
|
|
63
|
+
script_name = get_script_name(environ)
|
|
64
|
+
# If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
|
|
65
|
+
# trailing slash), operate as if '/' was requested.
|
|
66
|
+
path_info = get_path_info(environ) or "/"
|
|
67
|
+
self.environ = environ
|
|
68
|
+
self.path_info = path_info
|
|
69
|
+
# be careful to only replace the first slash in the path because of
|
|
70
|
+
# http://test/something and http://test//something being different as
|
|
71
|
+
# stated in RFC 3986.
|
|
72
|
+
self.path = "{}/{}".format(
|
|
73
|
+
script_name.rstrip("/"), path_info.replace("/", "", 1)
|
|
74
|
+
)
|
|
75
|
+
self.META = environ
|
|
76
|
+
self.META["PATH_INFO"] = path_info
|
|
77
|
+
self.META["SCRIPT_NAME"] = script_name
|
|
78
|
+
self.method = environ["REQUEST_METHOD"].upper()
|
|
79
|
+
# Set content_type, content_params, and encoding.
|
|
80
|
+
self._set_content_type_params(environ)
|
|
81
|
+
try:
|
|
82
|
+
content_length = int(environ.get("CONTENT_LENGTH"))
|
|
83
|
+
except (ValueError, TypeError):
|
|
84
|
+
content_length = 0
|
|
85
|
+
self._stream = LimitedStream(self.environ["wsgi.input"], content_length)
|
|
86
|
+
self._read_started = False
|
|
87
|
+
self.resolver_match = None
|
|
88
|
+
|
|
89
|
+
def __getstate__(self):
|
|
90
|
+
state = super().__getstate__()
|
|
91
|
+
for attr in self.meta_non_picklable_attrs:
|
|
92
|
+
if attr in state["META"]:
|
|
93
|
+
del state["META"][attr]
|
|
94
|
+
return state
|
|
95
|
+
|
|
96
|
+
def _get_scheme(self):
|
|
97
|
+
return self.environ.get("wsgi.url_scheme")
|
|
98
|
+
|
|
99
|
+
@cached_property
|
|
100
|
+
def GET(self):
|
|
101
|
+
# The WSGI spec says 'QUERY_STRING' may be absent.
|
|
102
|
+
raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "")
|
|
103
|
+
return QueryDict(raw_query_string, encoding=self._encoding)
|
|
104
|
+
|
|
105
|
+
def _get_post(self):
|
|
106
|
+
if not hasattr(self, "_post"):
|
|
107
|
+
self._load_post_and_files()
|
|
108
|
+
return self._post
|
|
109
|
+
|
|
110
|
+
def _set_post(self, post):
|
|
111
|
+
self._post = post
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def COOKIES(self):
|
|
115
|
+
raw_cookie = get_str_from_wsgi(self.environ, "HTTP_COOKIE", "")
|
|
116
|
+
return parse_cookie(raw_cookie)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def FILES(self):
|
|
120
|
+
if not hasattr(self, "_files"):
|
|
121
|
+
self._load_post_and_files()
|
|
122
|
+
return self._files
|
|
123
|
+
|
|
124
|
+
POST = property(_get_post, _set_post)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class WSGIHandler(base.BaseHandler):
|
|
128
|
+
request_class = WSGIRequest
|
|
129
|
+
|
|
130
|
+
def __init__(self, *args, **kwargs):
|
|
131
|
+
super().__init__(*args, **kwargs)
|
|
132
|
+
self.load_middleware()
|
|
133
|
+
|
|
134
|
+
def __call__(self, environ, start_response):
|
|
135
|
+
signals.request_started.send(sender=self.__class__, environ=environ)
|
|
136
|
+
request = self.request_class(environ)
|
|
137
|
+
response = self.get_response(request)
|
|
138
|
+
|
|
139
|
+
response._handler_class = self.__class__
|
|
140
|
+
|
|
141
|
+
status = "%d %s" % (response.status_code, response.reason_phrase)
|
|
142
|
+
response_headers = [
|
|
143
|
+
*response.items(),
|
|
144
|
+
*(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
|
|
145
|
+
]
|
|
146
|
+
start_response(status, response_headers)
|
|
147
|
+
if getattr(response, "file_to_stream", None) is not None and environ.get(
|
|
148
|
+
"wsgi.file_wrapper"
|
|
149
|
+
):
|
|
150
|
+
# If `wsgi.file_wrapper` is used the WSGI server does not call
|
|
151
|
+
# .close on the response, but on the file wrapper. Patch it to use
|
|
152
|
+
# response.close instead which takes care of closing all files.
|
|
153
|
+
response.file_to_stream.close = response.close
|
|
154
|
+
response = environ["wsgi.file_wrapper"](
|
|
155
|
+
response.file_to_stream, response.block_size
|
|
156
|
+
)
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_path_info(environ):
|
|
161
|
+
"""Return the HTTP request's PATH_INFO as a string."""
|
|
162
|
+
path_info = get_bytes_from_wsgi(environ, "PATH_INFO", "/")
|
|
163
|
+
|
|
164
|
+
return repercent_broken_unicode(path_info).decode()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_script_name(environ):
|
|
168
|
+
"""
|
|
169
|
+
Return the equivalent of the HTTP request's SCRIPT_NAME environment
|
|
170
|
+
variable. If Apache mod_rewrite is used, return what would have been
|
|
171
|
+
the script name prior to any rewriting (so it's the script name as seen
|
|
172
|
+
from the client's perspective).
|
|
173
|
+
"""
|
|
174
|
+
# If Apache's mod_rewrite had a whack at the URL, Apache set either
|
|
175
|
+
# SCRIPT_URL or REDIRECT_URL to the full resource URL before applying any
|
|
176
|
+
# rewrites. Unfortunately not every web server (lighttpd!) passes this
|
|
177
|
+
# information through all the time, so FORCE_SCRIPT_NAME, above, is still
|
|
178
|
+
# needed.
|
|
179
|
+
script_url = get_bytes_from_wsgi(environ, "SCRIPT_URL", "") or get_bytes_from_wsgi(
|
|
180
|
+
environ, "REDIRECT_URL", ""
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if script_url:
|
|
184
|
+
if b"//" in script_url:
|
|
185
|
+
# mod_wsgi squashes multiple successive slashes in PATH_INFO,
|
|
186
|
+
# do the same with script_url before manipulating paths (#17133).
|
|
187
|
+
script_url = _slashes_re.sub(b"/", script_url)
|
|
188
|
+
path_info = get_bytes_from_wsgi(environ, "PATH_INFO", "")
|
|
189
|
+
script_name = script_url.removesuffix(path_info)
|
|
190
|
+
else:
|
|
191
|
+
script_name = get_bytes_from_wsgi(environ, "SCRIPT_NAME", "")
|
|
192
|
+
|
|
193
|
+
return script_name.decode()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_bytes_from_wsgi(environ, key, default):
|
|
197
|
+
"""
|
|
198
|
+
Get a value from the WSGI environ dictionary as bytes.
|
|
199
|
+
|
|
200
|
+
key and default should be strings.
|
|
201
|
+
"""
|
|
202
|
+
value = environ.get(key, default)
|
|
203
|
+
# Non-ASCII values in the WSGI environ are arbitrarily decoded with
|
|
204
|
+
# ISO-8859-1. This is wrong for Plain websites where UTF-8 is the default.
|
|
205
|
+
# Re-encode to recover the original bytestring.
|
|
206
|
+
return value.encode("iso-8859-1")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_str_from_wsgi(environ, key, default):
|
|
210
|
+
"""
|
|
211
|
+
Get a value from the WSGI environ dictionary as str.
|
|
212
|
+
|
|
213
|
+
key and default should be str objects.
|
|
214
|
+
"""
|
|
215
|
+
value = get_bytes_from_wsgi(environ, key, default)
|
|
216
|
+
return value.decode(errors="replace")
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from plain.internal.legacy import management
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
# Automatically put the app dir on the Python path for convenience
|
|
8
|
+
app_dir = Path.cwd() / "app"
|
|
9
|
+
if app_dir.exists() and app_dir not in sys.path:
|
|
10
|
+
sys.path.insert(0, app_dir.as_posix())
|
|
11
|
+
|
|
12
|
+
management.execute_from_command_line()
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import pkgutil
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import (
|
|
6
|
+
_AppendConstAction,
|
|
7
|
+
_CountAction,
|
|
8
|
+
_StoreConstAction,
|
|
9
|
+
_SubParsersAction,
|
|
10
|
+
)
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from difflib import get_close_matches
|
|
13
|
+
from importlib import import_module
|
|
14
|
+
|
|
15
|
+
import plain.runtime
|
|
16
|
+
from plain.exceptions import ImproperlyConfigured
|
|
17
|
+
from plain.internal.legacy.management.base import (
|
|
18
|
+
BaseCommand,
|
|
19
|
+
CommandError,
|
|
20
|
+
CommandParser,
|
|
21
|
+
handle_default_options,
|
|
22
|
+
)
|
|
23
|
+
from plain.internal.legacy.management.color import color_style
|
|
24
|
+
from plain.packages import packages
|
|
25
|
+
from plain.runtime import settings
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_commands(management_dir):
|
|
29
|
+
"""
|
|
30
|
+
Given a path to a management directory, return a list of all the command
|
|
31
|
+
names that are available.
|
|
32
|
+
"""
|
|
33
|
+
command_dir = os.path.join(management_dir, "commands")
|
|
34
|
+
return [
|
|
35
|
+
name
|
|
36
|
+
for _, name, is_pkg in pkgutil.iter_modules([command_dir])
|
|
37
|
+
if not is_pkg and not name.startswith("_")
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_command_class(package_name, name):
|
|
42
|
+
"""
|
|
43
|
+
Given a command name and an application name, return the Command
|
|
44
|
+
class instance. Allow all errors raised by the import process
|
|
45
|
+
(ImportError, AttributeError) to propagate.
|
|
46
|
+
"""
|
|
47
|
+
module = import_module(f"{package_name}.management.commands.{name}")
|
|
48
|
+
return module.Command()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@functools.cache
|
|
52
|
+
def get_commands():
|
|
53
|
+
"""
|
|
54
|
+
Return a dictionary mapping command names to their callback applications.
|
|
55
|
+
|
|
56
|
+
Look for a management.commands package in plain.internal.legacy, and in each
|
|
57
|
+
installed application -- if a commands package exists, register all
|
|
58
|
+
commands in that package.
|
|
59
|
+
|
|
60
|
+
Core commands are always included. If a settings module has been
|
|
61
|
+
specified, also include user-defined commands.
|
|
62
|
+
|
|
63
|
+
The dictionary is in the format {command_name: package_name}. Key-value
|
|
64
|
+
pairs from this dictionary can then be used in calls to
|
|
65
|
+
load_command_class(package_name, command_name)
|
|
66
|
+
|
|
67
|
+
The dictionary is cached on the first call and reused on subsequent
|
|
68
|
+
calls.
|
|
69
|
+
"""
|
|
70
|
+
commands = {name: "plain.internal.legacy" for name in find_commands(__path__[0])}
|
|
71
|
+
|
|
72
|
+
if not settings.configured:
|
|
73
|
+
return commands
|
|
74
|
+
|
|
75
|
+
for package_config in reversed(packages.get_package_configs()):
|
|
76
|
+
path = os.path.join(package_config.path, "management")
|
|
77
|
+
commands.update({name: package_config.name for name in find_commands(path)})
|
|
78
|
+
|
|
79
|
+
return commands
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def call_command(command_name, *args, **options):
|
|
83
|
+
"""
|
|
84
|
+
Call the given command, with the given options and args/kwargs.
|
|
85
|
+
|
|
86
|
+
This is the primary API you should use for calling specific commands.
|
|
87
|
+
|
|
88
|
+
`command_name` may be a string or a command object. Using a string is
|
|
89
|
+
preferred unless the command object is required for further processing or
|
|
90
|
+
testing.
|
|
91
|
+
|
|
92
|
+
Some examples:
|
|
93
|
+
call_command('migrate')
|
|
94
|
+
call_command('shell', plain=True)
|
|
95
|
+
call_command('sqlmigrate', 'myapp')
|
|
96
|
+
|
|
97
|
+
from plain.internal.legacy.management.commands import flush
|
|
98
|
+
cmd = flush.Command()
|
|
99
|
+
call_command(cmd, verbosity=0, interactive=False)
|
|
100
|
+
# Do something with cmd ...
|
|
101
|
+
"""
|
|
102
|
+
if isinstance(command_name, BaseCommand):
|
|
103
|
+
# Command object passed in.
|
|
104
|
+
command = command_name
|
|
105
|
+
command_name = command.__class__.__module__.split(".")[-1]
|
|
106
|
+
else:
|
|
107
|
+
# Load the command object by name.
|
|
108
|
+
try:
|
|
109
|
+
package_name = get_commands()[command_name]
|
|
110
|
+
except KeyError:
|
|
111
|
+
raise CommandError("Unknown command: %r" % command_name)
|
|
112
|
+
|
|
113
|
+
if isinstance(package_name, BaseCommand):
|
|
114
|
+
# If the command is already loaded, use it directly.
|
|
115
|
+
command = package_name
|
|
116
|
+
else:
|
|
117
|
+
command = load_command_class(package_name, command_name)
|
|
118
|
+
|
|
119
|
+
# Simulate argument parsing to get the option defaults (see #10080 for details).
|
|
120
|
+
parser = command.create_parser("", command_name)
|
|
121
|
+
# Use the `dest` option name from the parser option
|
|
122
|
+
opt_mapping = {
|
|
123
|
+
min(s_opt.option_strings).lstrip("-").replace("-", "_"): s_opt.dest
|
|
124
|
+
for s_opt in parser._actions
|
|
125
|
+
if s_opt.option_strings
|
|
126
|
+
}
|
|
127
|
+
arg_options = {opt_mapping.get(key, key): value for key, value in options.items()}
|
|
128
|
+
parse_args = []
|
|
129
|
+
for arg in args:
|
|
130
|
+
if isinstance(arg, list | tuple):
|
|
131
|
+
parse_args += map(str, arg)
|
|
132
|
+
else:
|
|
133
|
+
parse_args.append(str(arg))
|
|
134
|
+
|
|
135
|
+
def get_actions(parser):
|
|
136
|
+
# Parser actions and actions from sub-parser choices.
|
|
137
|
+
for opt in parser._actions:
|
|
138
|
+
if isinstance(opt, _SubParsersAction):
|
|
139
|
+
for sub_opt in opt.choices.values():
|
|
140
|
+
yield from get_actions(sub_opt)
|
|
141
|
+
else:
|
|
142
|
+
yield opt
|
|
143
|
+
|
|
144
|
+
parser_actions = list(get_actions(parser))
|
|
145
|
+
mutually_exclusive_required_options = {
|
|
146
|
+
opt
|
|
147
|
+
for group in parser._mutually_exclusive_groups
|
|
148
|
+
for opt in group._group_actions
|
|
149
|
+
if group.required
|
|
150
|
+
}
|
|
151
|
+
# Any required arguments which are passed in via **options must be passed
|
|
152
|
+
# to parse_args().
|
|
153
|
+
for opt in parser_actions:
|
|
154
|
+
if opt.dest in options and (
|
|
155
|
+
opt.required or opt in mutually_exclusive_required_options
|
|
156
|
+
):
|
|
157
|
+
opt_dest_count = sum(v == opt.dest for v in opt_mapping.values())
|
|
158
|
+
if opt_dest_count > 1:
|
|
159
|
+
raise TypeError(
|
|
160
|
+
f"Cannot pass the dest {opt.dest!r} that matches multiple "
|
|
161
|
+
f"arguments via **options."
|
|
162
|
+
)
|
|
163
|
+
parse_args.append(min(opt.option_strings))
|
|
164
|
+
if isinstance(opt, _AppendConstAction | _CountAction | _StoreConstAction):
|
|
165
|
+
continue
|
|
166
|
+
value = arg_options[opt.dest]
|
|
167
|
+
if isinstance(value, list | tuple):
|
|
168
|
+
parse_args += map(str, value)
|
|
169
|
+
else:
|
|
170
|
+
parse_args.append(str(value))
|
|
171
|
+
defaults = parser.parse_args(args=parse_args)
|
|
172
|
+
defaults = dict(defaults._get_kwargs(), **arg_options)
|
|
173
|
+
# Raise an error if any unknown options were passed.
|
|
174
|
+
stealth_options = set(command.base_stealth_options + command.stealth_options)
|
|
175
|
+
dest_parameters = {action.dest for action in parser_actions}
|
|
176
|
+
valid_options = (dest_parameters | stealth_options).union(opt_mapping)
|
|
177
|
+
unknown_options = set(options) - valid_options
|
|
178
|
+
if unknown_options:
|
|
179
|
+
raise TypeError(
|
|
180
|
+
"Unknown option(s) for {} command: {}. Valid options are: {}.".format(
|
|
181
|
+
command_name,
|
|
182
|
+
", ".join(sorted(unknown_options)),
|
|
183
|
+
", ".join(sorted(valid_options)),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
# Move positional args out of options to mimic legacy optparse
|
|
187
|
+
args = defaults.pop("args", ())
|
|
188
|
+
if "skip_checks" not in options:
|
|
189
|
+
defaults["skip_checks"] = True
|
|
190
|
+
|
|
191
|
+
return command.execute(*args, **defaults)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ManagementUtility:
|
|
195
|
+
"""
|
|
196
|
+
Encapsulate the logic of the django-admin and manage.py utilities.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, argv=None):
|
|
200
|
+
self.argv = argv or sys.argv[:]
|
|
201
|
+
self.prog_name = os.path.basename(self.argv[0])
|
|
202
|
+
if self.prog_name == "__main__.py":
|
|
203
|
+
self.prog_name = "python -m django"
|
|
204
|
+
self.settings_exception = None
|
|
205
|
+
|
|
206
|
+
def main_help_text(self, commands_only=False):
|
|
207
|
+
"""Return the script's main help text, as a string."""
|
|
208
|
+
if commands_only:
|
|
209
|
+
usage = sorted(get_commands())
|
|
210
|
+
else:
|
|
211
|
+
usage = [
|
|
212
|
+
"",
|
|
213
|
+
"Type '%s help <subcommand>' for help on a specific subcommand."
|
|
214
|
+
% self.prog_name,
|
|
215
|
+
"",
|
|
216
|
+
"Available subcommands:",
|
|
217
|
+
]
|
|
218
|
+
commands_dict = defaultdict(lambda: [])
|
|
219
|
+
for name, app in get_commands().items():
|
|
220
|
+
if app == "plain.internal.legacy":
|
|
221
|
+
app = "django"
|
|
222
|
+
else:
|
|
223
|
+
app = app.rpartition(".")[-1]
|
|
224
|
+
commands_dict[app].append(name)
|
|
225
|
+
style = color_style()
|
|
226
|
+
for app in sorted(commands_dict):
|
|
227
|
+
usage.append("")
|
|
228
|
+
usage.append(style.NOTICE("[%s]" % app))
|
|
229
|
+
for name in sorted(commands_dict[app]):
|
|
230
|
+
usage.append(" %s" % name)
|
|
231
|
+
# Output an extra note if settings are not properly configured
|
|
232
|
+
if self.settings_exception is not None:
|
|
233
|
+
usage.append(
|
|
234
|
+
style.NOTICE(
|
|
235
|
+
"Note that only Plain core commands are listed "
|
|
236
|
+
"as settings are not properly configured (error: %s)."
|
|
237
|
+
% self.settings_exception
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return "\n".join(usage)
|
|
242
|
+
|
|
243
|
+
def fetch_command(self, subcommand):
|
|
244
|
+
"""
|
|
245
|
+
Try to fetch the given subcommand, printing a message with the
|
|
246
|
+
appropriate command called from the command line (usually
|
|
247
|
+
"django-admin" or "manage.py") if it can't be found.
|
|
248
|
+
"""
|
|
249
|
+
# Get commands outside of try block to prevent swallowing exceptions
|
|
250
|
+
commands = get_commands()
|
|
251
|
+
try:
|
|
252
|
+
package_name = commands[subcommand]
|
|
253
|
+
except KeyError:
|
|
254
|
+
if os.environ.get("PLAIN_SETTINGS_MODULE"):
|
|
255
|
+
# If `subcommand` is missing due to misconfigured settings, the
|
|
256
|
+
# following line will retrigger an ImproperlyConfigured exception
|
|
257
|
+
# (get_commands() swallows the original one) so the user is
|
|
258
|
+
# informed about it.
|
|
259
|
+
settings.INSTALLED_PACKAGES
|
|
260
|
+
elif not settings.configured:
|
|
261
|
+
sys.stderr.write("No Plain settings specified.\n")
|
|
262
|
+
possible_matches = get_close_matches(subcommand, commands)
|
|
263
|
+
sys.stderr.write("Unknown command: %r" % subcommand)
|
|
264
|
+
if possible_matches:
|
|
265
|
+
sys.stderr.write(". Did you mean %s?" % possible_matches[0])
|
|
266
|
+
sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name)
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
if isinstance(package_name, BaseCommand):
|
|
269
|
+
# If the command is already loaded, use it directly.
|
|
270
|
+
klass = package_name
|
|
271
|
+
else:
|
|
272
|
+
klass = load_command_class(package_name, subcommand)
|
|
273
|
+
return klass
|
|
274
|
+
|
|
275
|
+
def autocomplete(self):
|
|
276
|
+
"""
|
|
277
|
+
Output completion suggestions for BASH.
|
|
278
|
+
|
|
279
|
+
The output of this function is passed to BASH's `COMREPLY` variable and
|
|
280
|
+
treated as completion suggestions. `COMREPLY` expects a space
|
|
281
|
+
separated string as the result.
|
|
282
|
+
|
|
283
|
+
The `COMP_WORDS` and `COMP_CWORD` BASH environment variables are used
|
|
284
|
+
to get information about the cli input. Please refer to the BASH
|
|
285
|
+
man-page for more information about this variables.
|
|
286
|
+
|
|
287
|
+
Subcommand options are saved as pairs. A pair consists of
|
|
288
|
+
the long option string (e.g. '--exclude') and a boolean
|
|
289
|
+
value indicating if the option requires arguments. When printing to
|
|
290
|
+
stdout, an equal sign is appended to options which require arguments.
|
|
291
|
+
|
|
292
|
+
Note: If debugging this function, it is recommended to write the debug
|
|
293
|
+
output in a separate file. Otherwise the debug output will be treated
|
|
294
|
+
and formatted as potential completion suggestions.
|
|
295
|
+
"""
|
|
296
|
+
# Don't complete if user hasn't sourced bash_completion file.
|
|
297
|
+
if "DJANGO_AUTO_COMPLETE" not in os.environ:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
cwords = os.environ["COMP_WORDS"].split()[1:]
|
|
301
|
+
cword = int(os.environ["COMP_CWORD"])
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
curr = cwords[cword - 1]
|
|
305
|
+
except IndexError:
|
|
306
|
+
curr = ""
|
|
307
|
+
|
|
308
|
+
subcommands = [*get_commands(), "help"]
|
|
309
|
+
options = [("--help", False)]
|
|
310
|
+
|
|
311
|
+
# subcommand
|
|
312
|
+
if cword == 1:
|
|
313
|
+
print(" ".join(sorted(filter(lambda x: x.startswith(curr), subcommands))))
|
|
314
|
+
# subcommand options
|
|
315
|
+
# special case: the 'help' subcommand has no options
|
|
316
|
+
elif cwords[0] in subcommands and cwords[0] != "help":
|
|
317
|
+
subcommand_cls = self.fetch_command(cwords[0])
|
|
318
|
+
# special case: add the names of installed packages to options
|
|
319
|
+
if cwords[0] in ("dumpdata", "sqlmigrate", "sqlsequencereset", "test"):
|
|
320
|
+
try:
|
|
321
|
+
package_configs = packages.get_package_configs()
|
|
322
|
+
# Get the last part of the dotted path as the app name.
|
|
323
|
+
options.extend(
|
|
324
|
+
(package_config.label, 0) for package_config in package_configs
|
|
325
|
+
)
|
|
326
|
+
except ImportError:
|
|
327
|
+
# Fail silently if PLAIN_SETTINGS_MODULE isn't set. The
|
|
328
|
+
# user will find out once they execute the command.
|
|
329
|
+
pass
|
|
330
|
+
parser = subcommand_cls.create_parser("", cwords[0])
|
|
331
|
+
options.extend(
|
|
332
|
+
(min(s_opt.option_strings), s_opt.nargs != 0)
|
|
333
|
+
for s_opt in parser._actions
|
|
334
|
+
if s_opt.option_strings
|
|
335
|
+
)
|
|
336
|
+
# filter out previously specified options from available options
|
|
337
|
+
prev_opts = {x.split("=")[0] for x in cwords[1 : cword - 1]}
|
|
338
|
+
options = (opt for opt in options if opt[0] not in prev_opts)
|
|
339
|
+
|
|
340
|
+
# filter options by current input
|
|
341
|
+
options = sorted((k, v) for k, v in options if k.startswith(curr))
|
|
342
|
+
for opt_label, require_arg in options:
|
|
343
|
+
# append '=' to options which require args
|
|
344
|
+
if require_arg:
|
|
345
|
+
opt_label += "="
|
|
346
|
+
print(opt_label)
|
|
347
|
+
# Exit code of the bash completion function is never passed back to
|
|
348
|
+
# the user, so it's safe to always exit with 0.
|
|
349
|
+
# For more details see #25420.
|
|
350
|
+
sys.exit(0)
|
|
351
|
+
|
|
352
|
+
def execute(self):
|
|
353
|
+
"""
|
|
354
|
+
Given the command-line arguments, figure out which subcommand is being
|
|
355
|
+
run, create a parser appropriate to that command, and run it.
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
subcommand = self.argv[1]
|
|
359
|
+
except IndexError:
|
|
360
|
+
subcommand = "help" # Display help if no arguments were given.
|
|
361
|
+
|
|
362
|
+
# Preprocess options to extract --settings and --pythonpath.
|
|
363
|
+
# These options could affect the commands that are available, so they
|
|
364
|
+
# must be processed early.
|
|
365
|
+
parser = CommandParser(
|
|
366
|
+
prog=self.prog_name,
|
|
367
|
+
usage="%(prog)s subcommand [options] [args]",
|
|
368
|
+
add_help=False,
|
|
369
|
+
allow_abbrev=False,
|
|
370
|
+
)
|
|
371
|
+
parser.add_argument("--settings")
|
|
372
|
+
parser.add_argument("--pythonpath")
|
|
373
|
+
parser.add_argument("args", nargs="*") # catch-all
|
|
374
|
+
try:
|
|
375
|
+
options, args = parser.parse_known_args(self.argv[2:])
|
|
376
|
+
handle_default_options(options)
|
|
377
|
+
except CommandError:
|
|
378
|
+
pass # Ignore any option errors at this point.
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
settings.INSTALLED_PACKAGES
|
|
382
|
+
except ImproperlyConfigured as exc:
|
|
383
|
+
self.settings_exception = exc
|
|
384
|
+
except ImportError as exc:
|
|
385
|
+
self.settings_exception = exc
|
|
386
|
+
|
|
387
|
+
if settings.configured:
|
|
388
|
+
plain.runtime.setup()
|
|
389
|
+
|
|
390
|
+
self.autocomplete()
|
|
391
|
+
|
|
392
|
+
if subcommand == "help":
|
|
393
|
+
if "--commands" in args:
|
|
394
|
+
sys.stdout.write(self.main_help_text(commands_only=True) + "\n")
|
|
395
|
+
elif not options.args:
|
|
396
|
+
sys.stdout.write(self.main_help_text() + "\n")
|
|
397
|
+
else:
|
|
398
|
+
self.fetch_command(options.args[0]).print_help(
|
|
399
|
+
self.prog_name, options.args[0]
|
|
400
|
+
)
|
|
401
|
+
# Special-cases: We want 'django-admin --version' and
|
|
402
|
+
# 'django-admin --help' to work, for backwards compatibility.
|
|
403
|
+
elif subcommand == "version" or self.argv[1:] == ["--version"]:
|
|
404
|
+
sys.stdout.write(plain.runtime.__version__ + "\n")
|
|
405
|
+
elif self.argv[1:] in (["--help"], ["-h"]):
|
|
406
|
+
sys.stdout.write(self.main_help_text() + "\n")
|
|
407
|
+
else:
|
|
408
|
+
self.fetch_command(subcommand).run_from_argv(self.argv)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def execute_from_command_line(argv=None):
|
|
412
|
+
"""Run a ManagementUtility."""
|
|
413
|
+
utility = ManagementUtility(argv)
|
|
414
|
+
utility.execute()
|