cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a2__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 +2 -4
  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 +3 -3
  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 +1 -1
  16. cherrypy_foundation/plugins/ldap.py +25 -27
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +3 -14
  19. cherrypy_foundation/plugins/smtp.py +2 -8
  20. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
  23. cherrypy_foundation/plugins/tests/test_smtp.py +1 -31
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/templates/test_form.html +1 -7
  26. cherrypy_foundation/tests/test_error_page.py +1 -7
  27. cherrypy_foundation/tests/test_form.py +11 -40
  28. cherrypy_foundation/tests/test_passwd.py +2 -2
  29. cherrypy_foundation/tools/auth.py +27 -31
  30. cherrypy_foundation/tools/auth_mfa.py +83 -87
  31. cherrypy_foundation/tools/errors.py +27 -0
  32. cherrypy_foundation/tools/i18n.py +151 -235
  33. cherrypy_foundation/tools/jinja2.py +2 -15
  34. cherrypy_foundation/tools/ratelimit.py +18 -32
  35. cherrypy_foundation/tools/secure_headers.py +1 -1
  36. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  37. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  38. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  39. cherrypy_foundation/tools/tests/templates/test_jinja2.html +1 -2
  40. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  41. cherrypy_foundation/tools/tests/templates/test_jinjax.html +2 -3
  42. cherrypy_foundation/tools/tests/test_auth.py +3 -20
  43. cherrypy_foundation/tools/tests/test_auth_mfa.py +4 -6
  44. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  45. cherrypy_foundation/tools/tests/test_jinja2.py +5 -35
  46. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  47. cherrypy_foundation/url.py +25 -25
  48. cherrypy_foundation/widgets.py +2 -2
  49. cherrypy_foundation-1.0.0a2.dist-info/METADATA +42 -0
  50. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +53 -64
  51. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +1 -1
  52. cherrypy_foundation/components/Flash.jinja +0 -13
  53. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  54. cherrypy_foundation/components/LocaleSelection.js +0 -26
  55. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  56. cherrypy_foundation/sessions.py +0 -93
  57. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  58. cherrypy_foundation/tests/templates/test_url.html +0 -15
  59. cherrypy_foundation/tests/test_flash.py +0 -61
  60. cherrypy_foundation/tests/test_logging.py +0 -78
  61. cherrypy_foundation/tests/test_sessions.py +0 -89
  62. cherrypy_foundation/tests/test_url.py +0 -161
  63. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  64. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  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.0a2.dist-info}/licenses/LICENSE.md +0 -0
  68. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -150,27 +148,9 @@ class FileRateLimit(_DataStore):
150
148
  class Ratelimit(cherrypy.Tool):
151
149
  CONTEXT = 'TOOLS.RATELIMIT'
152
150
 
153
- _datastores = {}
154
-
155
151
  def __init__(self, priority=60):
156
152
  super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
157
153
 
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
154
  def check_ratelimit(
175
155
  self,
176
156
  delay=3600,
@@ -181,8 +161,7 @@ class Ratelimit(cherrypy.Tool):
181
161
  methods=None,
182
162
  debug=False,
183
163
  hit=1,
184
- storage_class=None,
185
- **storage_kwargs,
164
+ **conf,
186
165
  ):
187
166
  """
188
167
  Verify the ratelimit. By default return a 429 HTTP error code (Too Many Request). After 25 request within the same hour.
@@ -195,8 +174,9 @@ class Ratelimit(cherrypy.Tool):
195
174
  scope: if specify, define the scope of rate limit. Default to path_info.
196
175
  methods: if specify, only the methods in the list will be rate limited.
197
176
  """
198
- if delay <= 0:
199
- raise ValueError('Invalid delay: %s' % delay)
177
+ assert delay > 0, 'invalid delay'
178
+
179
+ # Check if limit is enabled
200
180
  if limit <= 0:
201
181
  return
202
182
 
@@ -207,8 +187,13 @@ class Ratelimit(cherrypy.Tool):
207
187
  cherrypy.log(f'skip rate limit for HTTP method {request.method}', context=self.CONTEXT)
208
188
  return
209
189
 
210
- # Create storage using storage class
211
- datastore = self._get_datastore(storage_class, **storage_kwargs)
190
+ # If datastore is not pass as configuration, create it for the first time.
191
+ datastore = getattr(self, '_ratelimit_datastore', None)
192
+ if datastore is None:
193
+ # Create storage using storage class
194
+ storage_class = conf.get('storage_class', RamRateLimit)
195
+ datastore = storage_class(**conf)
196
+ self._ratelimit_datastore = datastore
212
197
 
213
198
  # Identifier: prefer authenticated user; else client IP
214
199
  identifier = getattr(cherrypy.serving.request, 'login', None) or cherrypy.request.remote.ip
@@ -241,8 +226,7 @@ class Ratelimit(cherrypy.Tool):
241
226
  cherrypy.log(f'block access to path_info={request.path_info}', context=self.CONTEXT)
242
227
  if logout:
243
228
  if hasattr(cherrypy.serving, 'session'):
244
- with session_lock() as session:
245
- session.clear()
229
+ cherrypy.serving.session.clear()
246
230
  raise cherrypy.HTTPRedirect("/")
247
231
  raise cherrypy.HTTPError(return_status)
248
232
 
@@ -256,10 +240,12 @@ class Ratelimit(cherrypy.Tool):
256
240
 
257
241
  def reset(self):
258
242
  """
259
- Used to reset the ratelimit. (for unit test)
243
+ Used to reset the ratelimit.
260
244
  """
261
- for datastore in self._datastores.values():
262
- datastore.reset()
245
+ datastore = getattr(self, '_ratelimit_datastore', False)
246
+ if not datastore:
247
+ return
248
+ datastore.reset()
263
249
 
264
250
 
265
251
  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
@@ -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,4 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
1
+ <html>
3
2
  <head>
4
3
  <title>test-jinja2</title>
5
4
  </head>
@@ -0,0 +1,11 @@
1
+ <html lang="{{ get_translation().locale }}">
2
+ <head>
3
+ <title>test-jinja2</title>
4
+ </head>
5
+ <body>
6
+ {{ get_language_name(get_translation().locale) }}<br/>
7
+ {% trans %}Some text to translate{% endtrans %}<br/>
8
+ {{ my_datetime | format_datetime(format='full') }}<br/>
9
+ {{ my_date | format_date(format='full') }}
10
+ </body>
11
+ </html>
@@ -1,7 +1,6 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
1
+ <html>
3
2
  <head>
4
- <title>test-jinjax</title>
3
+ <title>test-jinja2</title>
5
4
  </head>
6
5
  <body>
7
6
  <Button link="http://example.com" label="foo" />
@@ -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):
@@ -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,6 +14,7 @@
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
+
17
18
  import datetime
18
19
  from collections import namedtuple
19
20
  from urllib.parse import urlencode
@@ -22,8 +23,6 @@ import cherrypy
22
23
  from cherrypy.lib.sessions import RamSession
23
24
  from cherrypy.test import helper
24
25
 
25
- from cherrypy_foundation.sessions import session_lock
26
-
27
26
  from ..auth import AUTH_LAST_PASSWORD_AT
28
27
  from ..auth_mfa import (
29
28
  MFA_CODE_TIME,
@@ -65,7 +64,7 @@ def user_from_key_func(userkey):
65
64
  return None
66
65
 
67
66
 
68
- @cherrypy.tools.sessions(locking='explicit')
67
+ @cherrypy.tools.sessions()
69
68
  @cherrypy.tools.auth(
70
69
  user_lookup_func=user_lookup_func,
71
70
  user_from_key_func=user_from_key_func,
@@ -104,8 +103,7 @@ class Root:
104
103
  code = cherrypy.tools.auth_mfa.generate_code()
105
104
  # Here the code should be send by email, SMS or any other means.
106
105
  # For our test we store it in the session.
107
- with session_lock() as session:
108
- session['code'] = code
106
+ cherrypy.serving.session['code'] = code
109
107
  html += "<p>new verification code sent to your email</p>\n"
110
108
  return html
111
109
 
@@ -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
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
1
+ # CherryPy Foundation
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
@@ -21,10 +21,6 @@ import cherrypy
21
21
  from cherrypy.test import helper
22
22
  from parameterized import parameterized
23
23
 
24
- from cherrypy_foundation.components import StaticMiddleware
25
- from cherrypy_foundation.tests import SeleniumUnitTest
26
- from cherrypy_foundation.url import url_for
27
-
28
24
  from .. import i18n # noqa
29
25
  from .. import jinja2 # noqa
30
26
 
@@ -42,18 +38,9 @@ def extra_processor():
42
38
  return {'var2': 'bar'}
43
39
 
44
40
 
45
- @cherrypy.tools.i18n(on=False)
46
- @cherrypy.tools.sessions(on=False)
47
- class Static:
48
-
49
- components = StaticMiddleware()
50
-
51
-
52
- @cherrypy.tools.jinja2(on=False, env=env, extra_processor=extra_processor)
41
+ @cherrypy.tools.jinja2(env=env, extra_processor=extra_processor)
53
42
  class Root:
54
43
 
55
- static = Static()
56
-
57
44
  @cherrypy.expose
58
45
  @cherrypy.tools.jinja2(template='test_jinja2.html')
59
46
  def index(self):
@@ -65,13 +52,12 @@ class Root:
65
52
  return {'var1': 'test-jinjax'}
66
53
 
67
54
  @cherrypy.expose
68
- @cherrypy.tools.jinja2(template='test_jinjax_i18n.html')
55
+ @cherrypy.tools.jinja2(template='test_jinja2_i18n.html')
69
56
  @cherrypy.tools.i18n(
70
57
  default='fr',
71
58
  default_timezone='America/Toronto',
72
59
  mo_dir=importlib.resources.files(__package__) / 'locales',
73
60
  domain='messages',
74
- cookie_name='locale', # For LocaleSelection
75
61
  )
76
62
  def localized(self):
77
63
  return {
@@ -80,7 +66,7 @@ class Root:
80
66
  }
81
67
 
82
68
 
83
- class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
69
+ class Jinja2Test(helper.CPWebCase):
84
70
  default_lang = None
85
71
  interactive = False
86
72
 
@@ -109,7 +95,6 @@ class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
109
95
  # Then the page is render dynamically using page context
110
96
  self.assertInBody('<a class="btn btn-primary" href="http://example.com">foo</a>')
111
97
 
112
- @skipUnless(HAS_JINJAX, reason='Required jinjax')
113
98
  @parameterized.expand(
114
99
  [
115
100
  ('server_default', {}, 'fr'),
@@ -136,18 +121,3 @@ class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
136
121
  self.assertInBody('Some text to translate')
137
122
  self.assertInBody('Wednesday, November 26, 2025, 6:16:00\u202fAM Eastern Standard Time')
138
123
  self.assertInBody('Monday, December 22, 2025')
139
-
140
- @skipUnless(HAS_JINJAX, reason='Required jinjax')
141
- def test_get_page_i18n_selenium(self):
142
- # Given a localized page render with jinja2
143
- with self.selenium() as driver:
144
- # When querying the page
145
- driver.get(url_for('localized'))
146
- # Then page load without error in english (enforced chronium lang)
147
- self.assertFalse(driver.get_log('browser'))
148
- self.assertEqual('en_US', driver.find_element('css selector', 'html').get_attribute('lang'))
149
- # When user select a language
150
- btn = driver.find_element('css selector', 'button[data-locale=fr]')
151
- btn.click()
152
- # Then page is reloaded with in French.
153
- self.assertEqual('fr', driver.find_element('css selector', 'html').get_attribute('lang'))
@@ -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(