cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a1__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.
Files changed (68) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +4 -5
  2. cherrypy_foundation/components/Datatable.jinja +2 -2
  3. cherrypy_foundation/components/Datatable.js +2 -2
  4. cherrypy_foundation/components/Field.jinja +6 -16
  5. cherrypy_foundation/components/Fields.jinja +2 -0
  6. cherrypy_foundation/components/Typeahead.css +1 -6
  7. cherrypy_foundation/components/Typeahead.jinja +2 -2
  8. cherrypy_foundation/components/__init__.py +2 -2
  9. cherrypy_foundation/components/tests/test_static.py +1 -1
  10. cherrypy_foundation/error_page.py +17 -20
  11. cherrypy_foundation/flash.py +15 -17
  12. cherrypy_foundation/form.py +2 -2
  13. cherrypy_foundation/logging.py +2 -2
  14. cherrypy_foundation/passwd.py +2 -2
  15. cherrypy_foundation/plugins/db.py +9 -35
  16. cherrypy_foundation/plugins/ldap.py +38 -46
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +84 -208
  19. cherrypy_foundation/plugins/smtp.py +46 -78
  20. cherrypy_foundation/plugins/tests/test_db.py +4 -4
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
  23. cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/test_error_page.py +1 -7
  26. cherrypy_foundation/tests/test_passwd.py +2 -2
  27. cherrypy_foundation/tools/auth.py +38 -59
  28. cherrypy_foundation/tools/auth_mfa.py +88 -89
  29. cherrypy_foundation/tools/errors.py +27 -0
  30. cherrypy_foundation/tools/i18n.py +153 -246
  31. cherrypy_foundation/tools/jinja2.py +13 -29
  32. cherrypy_foundation/tools/ratelimit.py +27 -37
  33. cherrypy_foundation/tools/secure_headers.py +5 -1
  34. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  35. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  36. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  37. cherrypy_foundation/tools/tests/test_auth.py +4 -21
  38. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  39. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  40. cherrypy_foundation/url.py +25 -25
  41. cherrypy_foundation/widgets.py +2 -2
  42. cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
  43. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
  44. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
  45. cherrypy_foundation/components/Flash.jinja +0 -13
  46. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  47. cherrypy_foundation/components/LocaleSelection.js +0 -26
  48. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  49. cherrypy_foundation/sessions.py +0 -93
  50. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  51. cherrypy_foundation/tests/templates/test_form.html +0 -16
  52. cherrypy_foundation/tests/templates/test_url.html +0 -15
  53. cherrypy_foundation/tests/test_flash.py +0 -61
  54. cherrypy_foundation/tests/test_form.py +0 -148
  55. cherrypy_foundation/tests/test_logging.py +0 -78
  56. cherrypy_foundation/tests/test_sessions.py +0 -89
  57. cherrypy_foundation/tests/test_url.py +0 -161
  58. cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
  59. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  60. cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
  61. cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
  62. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  63. cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
  64. cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
  65. cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
  66. cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
  67. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/licenses/LICENSE.md +0 -0
  68. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # Jinja2 tools for cherrypy
2
- # Copyright (C) 2021-2026 IKUS Software
2
+ # Copyright (C) 2021-2025 Patrik Dufresne
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -15,7 +15,6 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
17
  import importlib
18
- import logging
19
18
  import time
20
19
 
21
20
  import cherrypy
@@ -52,15 +51,15 @@ class Jinja2Tool(cherrypy.Tool):
52
51
 
53
52
  def wrap(*args, **kwargs):
54
53
  # Call original handler
55
- context = self.oldhandler(*args, **kwargs)
54
+ vars = self.oldhandler(*args, **kwargs)
56
55
  # Render template.
57
- return self.render_request(env=env, template=template, context=context, extra_processor=extra_processor)
56
+ return self.render_request(env=env, template=template, vars=vars, extra_processor=extra_processor)
58
57
 
59
58
  request = cherrypy.serving.request
60
59
  if request.handler is not None:
61
60
  # Replace request.handler with self
62
61
  if debug:
63
- cherrypy.log('replacing request handler', context='TOOLS.JINJA2', severity=logging.DEBUG)
62
+ cherrypy.log('Replacing request.handler', 'TOOLS.JINJA2')
64
63
  self.oldhandler = request.handler
65
64
  request.handler = wrap
66
65
 
@@ -80,27 +79,12 @@ class Jinja2Tool(cherrypy.Tool):
80
79
 
81
80
  # Enable translation if available
82
81
  if hasattr(cherrypy.tools, 'i18n'):
83
- from .i18n import (
84
- format_date,
85
- format_datetime,
86
- get_language_name,
87
- get_timezone_name,
88
- get_translation,
89
- list_available_locales,
90
- list_available_timezones,
91
- ugettext,
92
- ungettext,
93
- )
82
+ from .i18n import format_datetime, get_language_name, ugettext, ungettext
94
83
 
95
84
  env.add_extension('jinja2.ext.i18n')
96
85
  env.install_gettext_callables(ugettext, ungettext, newstyle=True)
97
- env.filters['format_date'] = format_date
98
86
  env.filters['format_datetime'] = format_datetime
99
87
  env.globals['get_language_name'] = get_language_name
100
- env.globals['get_translation'] = get_translation
101
- env.globals['list_available_locales'] = list_available_locales
102
- env.globals['list_available_timezones'] = list_available_timezones
103
- env.globals['get_timezone_name'] = get_timezone_name
104
88
 
105
89
  # Update globals, filters and tests
106
90
  env.globals.update(globals)
@@ -120,7 +104,7 @@ class Jinja2Tool(cherrypy.Tool):
120
104
 
121
105
  return env
122
106
 
123
- def render_request(self, template, context={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
107
+ def render_request(self, template, vars={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
124
108
  """
125
109
  Render template for a given cherrypy request.
126
110
  """
@@ -130,24 +114,24 @@ class Jinja2Tool(cherrypy.Tool):
130
114
  if extra_processor is _UNDEFINED:
131
115
  extra_processor = request.config.get('tools.jinja2.extra_processor')
132
116
  # Execute extra processor if defined.
133
- new_context = {}
117
+ all_vars = {}
134
118
  if extra_processor:
135
- new_context.update(extra_processor())
136
- new_context.update(context)
119
+ all_vars.update(extra_processor())
120
+ all_vars.update(vars)
137
121
  # Render templates
138
- return self.render(env=env, template=template, context=new_context)
122
+ return self.render(env=env, template=template, vars=all_vars)
139
123
 
140
- def render(self, env, template, context={}):
124
+ def render(self, env, template, vars={}):
141
125
  """
142
126
  Lower level function used to render a template using the given jinja2 environment, template(s) and variable context.
143
127
  """
144
128
  # Get the right templates
145
129
  if isinstance(template, (list, tuple)):
146
- names = [t.format(**context) for t in template]
130
+ names = [t.format(**vars) for t in template]
147
131
  tmpl = env.select_template(names)
148
132
  else:
149
133
  tmpl = env.get_template(template)
150
- out = tmpl.render(context)
134
+ out = tmpl.render(vars)
151
135
 
152
136
  # With JinjaX > 0.60 render explicitly here.
153
137
  if 'catalog' in env.globals and getattr(env.globals['catalog'], '_emit_assets_later', False):
@@ -1,5 +1,5 @@
1
1
  # Ratelimit tools for cherrypy
2
- # Copyright (C) 2022-2026 IKUS Software
2
+ # Copyright (C) 2022-2025 Patrik Dufresne
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -22,8 +22,6 @@ from collections import namedtuple
22
22
 
23
23
  import cherrypy
24
24
 
25
- from cherrypy_foundation.sessions import session_lock
26
-
27
25
  Tracker = namedtuple('Tracker', ['token', 'hits', 'timeout'])
28
26
 
29
27
 
@@ -148,29 +146,9 @@ class FileRateLimit(_DataStore):
148
146
 
149
147
 
150
148
  class Ratelimit(cherrypy.Tool):
151
- CONTEXT = 'TOOLS.RATELIMIT'
152
-
153
- _datastores = {}
154
-
155
149
  def __init__(self, priority=60):
156
150
  super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
157
151
 
158
- def _get_datastore(self, storage_class, **kwargs):
159
- """
160
- Create new datastore or return existing datastore.
161
- """
162
- # Default to RAM if storage class is not defined.
163
- if storage_class is None:
164
- kwargs = {}
165
- storage_class = RamRateLimit
166
- # Lookup for matching storage.
167
- key = (storage_class, str(kwargs))
168
- datastore = self._datastores.get(key)
169
- if datastore is None:
170
- # Create new storage if not found.
171
- self._datastores[key] = datastore = storage_class(**kwargs)
172
- return datastore
173
-
174
152
  def check_ratelimit(
175
153
  self,
176
154
  delay=3600,
@@ -181,8 +159,7 @@ class Ratelimit(cherrypy.Tool):
181
159
  methods=None,
182
160
  debug=False,
183
161
  hit=1,
184
- storage_class=None,
185
- **storage_kwargs,
162
+ **conf,
186
163
  ):
187
164
  """
188
165
  Verify the ratelimit. By default return a 429 HTTP error code (Too Many Request). After 25 request within the same hour.
@@ -195,8 +172,9 @@ class Ratelimit(cherrypy.Tool):
195
172
  scope: if specify, define the scope of rate limit. Default to path_info.
196
173
  methods: if specify, only the methods in the list will be rate limited.
197
174
  """
198
- if delay <= 0:
199
- raise ValueError('Invalid delay: %s' % delay)
175
+ assert delay > 0, 'invalid delay'
176
+
177
+ # Check if limit is enabled
200
178
  if limit <= 0:
201
179
  return
202
180
 
@@ -204,11 +182,19 @@ class Ratelimit(cherrypy.Tool):
204
182
  request = cherrypy.request
205
183
  if methods is not None and request.method not in methods:
206
184
  if debug:
207
- cherrypy.log(f'skip rate limit for HTTP method {request.method}', context=self.CONTEXT)
185
+ cherrypy.log(
186
+ 'skip rate limit for HTTP method %s' % (request.method,),
187
+ 'TOOLS.RATELIMIT',
188
+ )
208
189
  return
209
190
 
210
- # Create storage using storage class
211
- datastore = self._get_datastore(storage_class, **storage_kwargs)
191
+ # If datastore is not pass as configuration, create it for the first time.
192
+ datastore = getattr(self, '_ratelimit_datastore', None)
193
+ if datastore is None:
194
+ # Create storage using storage class
195
+ storage_class = conf.get('storage_class', RamRateLimit)
196
+ datastore = storage_class(**conf)
197
+ self._ratelimit_datastore = datastore
212
198
 
213
199
  # Identifier: prefer authenticated user; else client IP
214
200
  identifier = getattr(cherrypy.serving.request, 'login', None) or cherrypy.request.remote.ip
@@ -227,7 +213,10 @@ class Ratelimit(cherrypy.Tool):
227
213
  # Get hits count using datastore.
228
214
  hits, timeout = datastore.get_and_increment(token, delay, hit)
229
215
  if debug:
230
- cherrypy.log(f'check and increase limit token={token} limit={limit} hits={hits}', context=self.CONTEXT)
216
+ cherrypy.log(
217
+ 'check and increase rate limit for scope %s, limit %s, hits %s' % (token, limit, hits),
218
+ 'TOOLS.RATELIMIT',
219
+ )
231
220
 
232
221
  # Verify user has not exceeded rate limit
233
222
  remaining = max(0, limit - hits)
@@ -238,11 +227,10 @@ class Ratelimit(cherrypy.Tool):
238
227
  cherrypy.response.headers['X-RateLimit-Reset'] = str(timeout)
239
228
 
240
229
  if limit < hits: # block only after 'limit' successful requests
241
- cherrypy.log(f'block access to path_info={request.path_info}', context=self.CONTEXT)
230
+ cherrypy.log('ratelimit access to `%s`' % request.path_info, 'TOOLS.RATELIMIT')
242
231
  if logout:
243
232
  if hasattr(cherrypy.serving, 'session'):
244
- with session_lock() as session:
245
- session.clear()
233
+ cherrypy.serving.session.clear()
246
234
  raise cherrypy.HTTPRedirect("/")
247
235
  raise cherrypy.HTTPError(return_status)
248
236
 
@@ -256,10 +244,12 @@ class Ratelimit(cherrypy.Tool):
256
244
 
257
245
  def reset(self):
258
246
  """
259
- Used to reset the ratelimit. (for unit test)
247
+ Used to reset the ratelimit.
260
248
  """
261
- for datastore in self._datastores.values():
262
- datastore.reset()
249
+ datastore = getattr(self, '_ratelimit_datastore', False)
250
+ if not datastore:
251
+ return
252
+ datastore.reset()
263
253
 
264
254
 
265
255
  cherrypy.tools.ratelimit = Ratelimit()
@@ -1,5 +1,5 @@
1
1
  # Secure Headers tool for cherrypy
2
- # Copyright (C) 2012-2026 IKUS Software
2
+ # Copyright (C) 2012-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -15,9 +15,13 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
17
  import http.cookies
18
+ import logging
18
19
 
19
20
  import cherrypy
20
21
 
22
+ # Define the logger
23
+ logger = logging.getLogger(__name__)
24
+
21
25
  DEFAULT_CSP = {
22
26
  'default-src': 'self',
23
27
  'style-src': ('self', 'unsafe-inline'),
@@ -1,5 +1,5 @@
1
1
  # Session timeout tool for cherrypy
2
- # Copyright (C) 2012-2026 IKUS Software
2
+ # Copyright (C) 2012-2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -21,8 +21,6 @@ import time
21
21
  import cherrypy
22
22
  from cherrypy.lib import httputil
23
23
 
24
- from cherrypy_foundation.sessions import session_lock
25
-
26
24
  SESSION_PERSISTENT = '_session_persistent'
27
25
  SESSION_START_TIME = '_session_start_time'
28
26
 
@@ -59,13 +57,13 @@ class SessionsTimeout(cherrypy.Tool):
59
57
  absolute_timeout = int(cfg.get('tools.sessions_timeout.absolute_timeout', SESSION_DEFAULT_ABSOLUTE_TIMEOUT))
60
58
  return idle_timeout, persistent_timeout, absolute_timeout
61
59
 
62
- def _set_cookie_max_age(self, session, max_age: int) -> None:
60
+ def _set_cookie_max_age(self, max_age: int) -> None:
63
61
  """Adjust only Max-Age and Expires for the session cookie, keep other flags intact."""
64
62
  cookie_name = self._cookie_name()
65
63
  cookie = cherrypy.serving.response.cookie
66
64
  # Ensure the cookie key exists (CherryPy sets it when session id is issued/regenerated).
67
65
  if cookie_name not in cookie:
68
- cookie[cookie_name] = session.id
66
+ cookie[cookie_name] = cherrypy.serving.session.id
69
67
  cookie[cookie_name]['max-age'] = str(max(0, max_age))
70
68
  cookie[cookie_name]['expires'] = httputil.HTTPDate(time.time() + max(0, max_age))
71
69
 
@@ -75,21 +73,24 @@ class SessionsTimeout(cherrypy.Tool):
75
73
  return 0
76
74
  return int(math.ceil(seconds / 60.0))
77
75
 
78
- def _ensure_start_time(self, session) -> None:
76
+ def _ensure_start_time(self) -> None:
79
77
  """Ensure the session has a stable start time anchor for absolute timeout."""
78
+ session = cherrypy.serving.session
80
79
  if SESSION_START_TIME not in session or not isinstance(session.get(SESSION_START_TIME), datetime.datetime):
81
80
  session[SESSION_START_TIME] = session.now()
82
81
 
83
- def _expire_and_restart(self, session, make_persistent: bool | None = None) -> None:
82
+ def _expire_and_restart(self, make_persistent: bool | None = None) -> None:
84
83
  """Expire current session and start a fresh one. Optionally set persistent flag."""
84
+ session = cherrypy.serving.session
85
85
  session.clear()
86
86
  session.regenerate()
87
87
  session[SESSION_START_TIME] = session.now()
88
88
  if make_persistent is not None:
89
89
  session[SESSION_PERSISTENT] = bool(make_persistent)
90
90
 
91
- def _update_session_timeout(self, session) -> None:
92
- self._ensure_start_time(session)
91
+ def _update_session_timeout(self) -> None:
92
+ session = cherrypy.serving.session
93
+ self._ensure_start_time()
93
94
 
94
95
  now: datetime.datetime = session.now()
95
96
  start: datetime.datetime = session[SESSION_START_TIME]
@@ -119,9 +120,9 @@ class SessionsTimeout(cherrypy.Tool):
119
120
  # Expired due to either sliding window or absolute cap
120
121
  # Start a fresh session; do NOT automatically re-mark persistent here.
121
122
  # Authentication/Remember me logic elsewhere can call set_persistent(True) again after login.
122
- self._expire_and_restart(session)
123
+ self._expire_and_restart()
123
124
  # After regeneration, set a sensible session.timeout baseline:
124
- session.timeout = idle_timeout_min
125
+ cherrypy.serving.session.timeout = idle_timeout_min
125
126
  return
126
127
 
127
128
  # Apply per-request remaining minutes to CherryPy's session timeout
@@ -129,7 +130,7 @@ class SessionsTimeout(cherrypy.Tool):
129
130
 
130
131
  if is_persistent:
131
132
  # Keep cookie lifetime aligned with remaining time so it can survive browser restarts.
132
- self._set_cookie_max_age(session, remaining_sec)
133
+ self._set_cookie_max_age(remaining_sec)
133
134
  else:
134
135
  # For non-persistent, let CherryPy manage the session cookie (session or transient cookie).
135
136
  # If you want strict enforcement client-side too, uncomment the next line:
@@ -142,26 +143,23 @@ class SessionsTimeout(cherrypy.Tool):
142
143
  return
143
144
 
144
145
  # Ensure configured values are honored (method signature kept for CherryPy Tool interface)
145
- with session_lock() as session:
146
- self._update_session_timeout(session)
146
+ self._update_session_timeout()
147
147
 
148
148
  def set_persistent(self, value: bool = True) -> None:
149
149
  """Mark current session as persistent (or not) and reset the start window."""
150
- with session_lock() as session:
151
- session[SESSION_PERSISTENT] = bool(value)
152
- # Reset absolute window anchor when the persistence policy changes
153
- session[SESSION_START_TIME] = session.now()
154
- self._update_session_timeout(session)
150
+ session = cherrypy.serving.session
151
+ session[SESSION_PERSISTENT] = bool(value)
152
+ # Reset absolute window anchor when the persistence policy changes
153
+ session[SESSION_START_TIME] = session.now()
154
+ self._update_session_timeout()
155
155
 
156
156
  def is_persistent(self) -> bool:
157
157
  """Return True if the session is marked persistent."""
158
- with session_lock() as session:
159
- return bool(session.get(SESSION_PERSISTENT, False))
158
+ return bool(cherrypy.serving.session.get(SESSION_PERSISTENT, False))
160
159
 
161
160
  def get_session_start_time(self) -> datetime.datetime | None:
162
161
  """Return the session start time if any."""
163
- with session_lock() as session:
164
- return session.get(SESSION_START_TIME, None)
162
+ return cherrypy.serving.session.get(SESSION_START_TIME, None)
165
163
 
166
164
 
167
165
  cherrypy.tools.sessions_timeout = SessionsTimeout()
@@ -5,11 +5,11 @@ msgstr ""
5
5
  "PO-Revision-Date: \n"
6
6
  "Last-Translator: \n"
7
7
  "Language-Team: \n"
8
- "Language: de\n"
8
+ "Language: en\n"
9
9
  "MIME-Version: 1.0\n"
10
10
  "Content-Type: text/plain; charset=UTF-8\n"
11
11
  "Content-Transfer-Encoding: 8bit\n"
12
12
  "X-Generator: Poedit 3.6\n"
13
13
 
14
14
  msgid "Some text to translate"
15
- msgstr "Einige zu übersetzende Texte"
15
+ msgstr ""
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -14,12 +14,11 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
- import tempfile
17
+
18
18
  from collections import namedtuple
19
19
  from urllib.parse import urlencode
20
20
 
21
21
  import cherrypy
22
- from cherrypy.lib.sessions import FileSession
23
22
  from cherrypy.test import helper
24
23
 
25
24
  from .. import auth # noqa
@@ -43,7 +42,7 @@ def user_from_key_func(userkey):
43
42
  return None
44
43
 
45
44
 
46
- @cherrypy.tools.sessions(locking='explicit', storage_class=FileSession)
45
+ @cherrypy.tools.sessions()
47
46
  @cherrypy.tools.auth(
48
47
  user_lookup_func=user_lookup_func,
49
48
  user_from_key_func=user_from_key_func,
@@ -69,24 +68,8 @@ class Root:
69
68
  class AuthManagerTest(helper.CPWebCase):
70
69
  interactive = False
71
70
 
72
- @classmethod
73
- def setup_class(cls):
74
- cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-session')
75
- cls.session_dir = cls.tempdir.name
76
- super().setup_class()
77
-
78
- @classmethod
79
- def teardown_class(cls):
80
- cls.tempdir.cleanup()
81
- super().teardown_class()
82
-
83
71
  @classmethod
84
72
  def setup_server(cls):
85
- cherrypy.config.update(
86
- {
87
- 'tools.sessions.storage_path': cls.session_dir,
88
- }
89
- )
90
73
  cherrypy.tree.mount(Root(), '/')
91
74
 
92
75
  def test_auth_redirect(self):
@@ -100,7 +83,7 @@ class AuthManagerTest(helper.CPWebCase):
100
83
  def test_auth_login(self):
101
84
  # Given unauthenticated user
102
85
  # When posting valid login
103
- self.getPage('/login/', method='POST', body=urlencode({'username': 'myuser', 'password': 'changeme'}))
86
+ self.getPage('/login/', method='POST', body=urlencode([('username', 'myuser'), ('password', 'changeme')]))
104
87
  # Then user is redirect to index.
105
88
  self.assertStatus(303)
106
89
  self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -38,6 +38,7 @@ class TestI18n(unittest.TestCase):
38
38
  def test_search_translation_en(self):
39
39
  # Load default translation return translation
40
40
  t = i18n._search_translation(self.mo_dir, 'messages', 'en')
41
+ self.assertIsInstance(t, gettext.GNUTranslations)
41
42
  self.assertEqual("en", t.locale.language)
42
43
  # Test translation object
43
44
  self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
@@ -53,8 +54,7 @@ class TestI18n(unittest.TestCase):
53
54
  def test_search_translation_invalid(self):
54
55
  # Load invalid translation return None
55
56
  t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
56
- # Return Null translations.
57
- self.assertIn('NullTranslations', str(t.__class__))
57
+ self.assertIsNone(t)
58
58
 
59
59
 
60
60
  class Root:
@@ -76,7 +76,6 @@ class AbstractI18nTest(helper.CPWebCase):
76
76
  'tools.i18n.default': cls.default_lang,
77
77
  'tools.i18n.mo_dir': importlib.resources.files(__package__) / 'locales',
78
78
  'tools.i18n.domain': 'messages',
79
- 'tools.i18n.cookie_name': 'locale',
80
79
  }
81
80
  )
82
81
  cherrypy.tree.mount(Root(), '/')
@@ -84,13 +83,6 @@ class AbstractI18nTest(helper.CPWebCase):
84
83
 
85
84
  class TestI18nWebCase(AbstractI18nTest):
86
85
 
87
- def test_language_with_invalid(self):
88
- # Query the page without login-in
89
- self.getPage("/", headers=[("Accept-Language", "invalid")])
90
- self.assertStatus('200 OK')
91
- self.assertHeaderItemValue("Content-Language", "en")
92
- self.assertInBody(TEXT_EN)
93
-
94
86
  def test_language_with_unknown(self):
95
87
  # Query the page without login-in
96
88
  self.getPage("/", headers=[("Accept-Language", "it")])
@@ -101,13 +93,13 @@ class TestI18nWebCase(AbstractI18nTest):
101
93
  def test_language_en(self):
102
94
  self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
103
95
  self.assertStatus('200 OK')
104
- self.assertHeaderItemValue("Content-Language", "en-US")
96
+ self.assertHeaderItemValue("Content-Language", "en")
105
97
  self.assertInBody(TEXT_EN)
106
98
 
107
99
  def test_language_en_fr(self):
108
100
  self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8,fr-CA;q=0.8")])
109
101
  self.assertStatus('200 OK')
110
- self.assertHeaderItemValue("Content-Language", "en-US")
102
+ self.assertHeaderItemValue("Content-Language", "en")
111
103
  self.assertInBody(TEXT_EN)
112
104
 
113
105
  def test_language_fr(self):
@@ -115,22 +107,8 @@ class TestI18nWebCase(AbstractI18nTest):
115
107
  self.assertInBody(TEXT_EN)
116
108
  self.getPage("/", headers=[("Accept-Language", "fr-CA;q=0.8,fr;q=0.6")])
117
109
  self.assertStatus('200 OK')
118
- self.assertHeaderItemValue("Content-Language", "fr-CA")
119
- self.assertInBody(TEXT_FR)
120
-
121
- def test_language_en_US_POSIX(self):
122
- # When calling with locale variant
123
- self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
124
- self.assertStatus('200 OK')
125
- # Tehn page return en-US
126
- self.assertHeaderItemValue("Content-Language", "en-US")
127
-
128
- def test_cookie_fr(self):
129
- # When calling with locale variant
130
- self.getPage("/", headers=[("Accept-Language", "en-US"), ("Cookie", "locale=fr")])
131
- self.assertStatus('200 OK')
132
- # Tehn page return en-US
133
110
  self.assertHeaderItemValue("Content-Language", "fr")
111
+ self.assertInBody(TEXT_FR)
134
112
 
135
113
  def test_with_preferred_lang(self):
136
114
  # Given a default lang 'en'
@@ -147,59 +125,6 @@ class TestI18nWebCase(AbstractI18nTest):
147
125
  self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
148
126
  self.assertIn('March', i18n.format_datetime(date, format='long'))
149
127
 
150
- def test_format_datetime_locales(self):
151
- date = datetime.fromtimestamp(1680111611, timezone.utc)
152
- with i18n.preferred_timezone('utc'):
153
- with i18n.preferred_lang('fr'):
154
- self.assertEqual('29 mars 2023, 17:40:11 TU', i18n.format_datetime(date, format='long'))
155
- with i18n.preferred_lang('en'):
156
- self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
157
- with i18n.preferred_lang('en_US'):
158
- self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
159
- with i18n.preferred_lang('en_GB'):
160
- self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
161
- with i18n.preferred_lang('en_CH'):
162
- self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
163
- with i18n.preferred_lang('de'):
164
- self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
165
- with i18n.preferred_lang('de_CH'):
166
- self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
167
-
168
- with i18n.preferred_timezone('Europe/Paris'):
169
- with i18n.preferred_lang('fr'):
170
- self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
171
- with i18n.preferred_lang('fr_CH'):
172
- self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
173
- with i18n.preferred_lang('en'):
174
- self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
175
- with i18n.preferred_lang('en_US'):
176
- self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
177
- with i18n.preferred_lang('en_GB'):
178
- self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
179
- with i18n.preferred_lang('en_CH'):
180
- self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
181
- with i18n.preferred_lang('de'):
182
- self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
183
- with i18n.preferred_lang('de_CH'):
184
- self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
185
-
186
- def test_list_available_locales(self):
187
- self.assertEqual(['de', 'en', 'fr'], sorted([str(l) for l in i18n.list_available_locales()]))
188
-
189
- def test_list_available_timezones(self):
190
- timezones = i18n.list_available_timezones()
191
- self.assertIn('America/Toronto', timezones)
192
-
193
- def test_get_timezone_name(self):
194
- with i18n.preferred_lang('en'):
195
- self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto'))
196
- self.assertEqual('ET', i18n.get_timezone_name('America/Toronto', width='short'))
197
- self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto', width='long'))
198
- with i18n.preferred_lang('fr'):
199
- self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto'))
200
- self.assertEqual('HE', i18n.get_timezone_name('America/Toronto', width='short'))
201
- self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto', width='long'))
202
-
203
128
 
204
129
  class TestI18nDefaultLangWebCase(AbstractI18nTest):
205
130
  default_lang = 'FR'
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2026 IKUS Software
2
+ # Copyright (C) 2025 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -49,7 +49,7 @@ class RateLimitTest(helper.CPWebCase):
49
49
 
50
50
  @classmethod
51
51
  def setup_server(cls):
52
- rate_limit_storage_class = None
52
+ rate_limit_storage_class = ratelimit.RamRateLimit
53
53
  if cls.rate_limit_dir:
54
54
  rate_limit_storage_class = ratelimit.FileRateLimit
55
55
  cherrypy.config.update(