warnings
frequenz.core.warnings ¤
Warnings utilities.
This module provides ignoring_warnings, a context manager to
silence warnings around a piece of code, without the side effect that
warnings.catch_warnings has, and
ignoring_deprecations for the common case of a library
having to touch a symbol it deprecated itself.
It also provides deprecated_aliases, to keep the old import
path of a symbol that moved to another module, or was renamed, working, warning
whoever uses it since which version it is deprecated, as its
DeprecatedAlias entry says, and
asserting_no_warnings with its
asserting_no_deprecations shortcut, to check in a test
that a piece of code doesn't warn.
The documented way of silencing a warning locally is a
warnings.catch_warnings block, but merely entering and leaving one invalidates
the warnings deduplication history of the whole program, so every warning that was
already shown is shown again, and again on every call. This is
python/cpython#73858, open since
2017.
ignoring_warnings adds the filter to the list the interpreter
consults and takes it out again on exit, which leaves the deduplication history
untouched, for about half a microsecond more per block than the standard library one
costs:
import warnings
from frequenz.core.warnings import ignoring_warnings
def legacy_convert(value: str) -> int:
warnings.warn("legacy_convert() is deprecated", DeprecationWarning, stacklevel=2)
return int(value)
def convert(value: str) -> int:
with ignoring_warnings(category=DeprecationWarning):
return legacy_convert(value)
with warnings.catch_warnings(record=True, action="default") as caught:
for _ in range(10):
warnings.warn("shown only once", UserWarning)
convert("1")
assert len(caught) == 1
With warnings.catch_warnings in convert() the same loop shows the
UserWarning ten times.
Classes¤
frequenz.core.warnings.DeprecatedAlias
dataclass
¤
A symbol kept reachable from its old name.
Each one is an entry for deprecated_aliases, which
has the examples.
The symbol moved to another module, new_module, was renamed, new_name, or
both. Without new_module it is still in the module defining the alias, under
its new name.
An alias says either since which version it is deprecated, with since, or
its whole warning message, with message, but not both. since="v1.2.0"
warns with <old> is deprecated since v1.2.0. Use <new> instead., which is
what the deprecations guide suggests, so message is only needed to say
something else.
Everything is checked when the alias is created rather than when it is reached.
Source code in src/frequenz/core/warnings/_deprecated_aliases.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
Attributes¤
message
instance-attribute
¤
message: str | None
The template for the warning message, or None if it has a since.
It is formatted with {old} and {new}, the fully qualified names of the
alias and of the symbol it resolves to, as plain fields, without attributes,
indexes, conversions or format specs.
new_module
instance-attribute
¤
new_module: str | None
The fully qualified name of the module the symbol lives in now.
None if it is still in the module defining the alias, under
new_name.
new_name
instance-attribute
¤
new_name: str
The name the symbol has now, name if it was not renamed.
since
instance-attribute
¤
since: str | None
The version the alias is deprecated since, or None if it has a message.
The warning says <old> is deprecated since <since>. Use <new> instead.
Methods:¤
__init__ ¤
__init__(
name: str,
/,
*,
new_module: str | None = None,
new_name: str | None = None,
since: str | None = None,
message: str | None = None,
) -> None
Initialize this instance.
At least one of new_module and new_name must be given, and exactly one
of since and message.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The deprecated name, in the module defining the alias.
TYPE:
|
new_module
|
The fully qualified name of the module the symbol lives in now, if it moved out of the module defining the alias.
TYPE:
|
new_name
|
The name the symbol has now, if it was renamed.
TYPE:
|
since
|
The version the alias is deprecated since, such as
TYPE:
|
message
|
The template for the whole warning message instead, formatted
with
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If an argument is not a string, if neither |
ValueError
|
If |
Source code in src/frequenz/core/warnings/_deprecated_aliases.py
format_message ¤
Format the warning message for this alias.
| PARAMETER | DESCRIPTION |
|---|---|
old
|
The fully qualified name of the alias.
TYPE:
|
new
|
The fully qualified name of the symbol it resolves to.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
|
Source code in src/frequenz/core/warnings/_deprecated_aliases.py
frequenz.core.warnings.asserting_no_warnings ¤
Bases: AbstractContextManager[None]
A context manager that fails if a matching warning is raised inside its block.
Use this to test that a call doesn't warn. The usual alternative, an
"error" filter, makes warnings.warn raise an exception inside the
code under test. A broad except in that code can then swallow it or turn
it into an unrelated failure, and the code no longer behaves as it would in
production. This block records warnings instead and leaves the code alone.
When the block ends, it raises AssertionError listing every
unexpected warning and where it came from.
Warnings that don't cause a failure are still shown as usual when the block
ends. The unexpected ones are shown too if the block is left because of an
exception. In that case the exception is raised instead of the
AssertionError, since it's the bigger problem, but the warnings aren't
lost.
Resets the warnings deduplication history
Recording needs warnings.catch_warnings, and using it resets that
history for the whole program. There's no way around it here: a warning
that was already shown is skipped (by the module's
__warningregistry__) before the filters are even checked, so the only
way to catch it again is to reset the history.
In tests this is usually fine, since they are isolated. Anywhere else, every warning shown before the block will be shown again after it. So use this in a short test that checks just this, not in a hot path, a long-running process, or around a whole test suite.
Concurrency
warnings.catch_warnings affects the whole process unless
sys.flags.context_aware_warnings is on. That flag was added in Python
3.14 and is off by default, except in free-threaded builds. Without it,
a matching warning from another thread, or from another asyncio task
while the block is waiting on an await, is recorded here and fails the
block, even if the code under test never warned. Use it only around
synchronous code, and not where background threads can raise matching
warnings.
With the flag on, the problem is smaller but not gone. New threads start with an empty context, so their warnings aren't recorded. Tasks created inside the block, however, inherit its recording and can still fail it.
An ignoring_warnings block inside wins
ignoring_warnings puts its filter in front of
the one this block adds. So if the code under test ignores a warning
itself, that warning is never recorded and can't fail the block. This is
intended, since the code explicitly asked to ignore it. It does mean
that an asserting_no_deprecations block
around code using ignoring_deprecations
internally only checks the deprecations the code didn't silence, not all
of them.
Handlers installed inside the block
While the block is active, it controls how warnings are shown. Handlers
installed before the block, like the one logging.captureWarnings
sets up, keep working: once the block ends, they receive the warnings
that didn't fail it. But if the code under test replaces
warnings.showwarning itself, any warning raised after that goes
straight to its own handler, and this block can't see it or fail on it.
A plain catch_warnings(record=True) has the same limitation.
Not reentrant
Entering an instance that is already inside its block raises
RuntimeError.
Example
Source code in src/frequenz/core/warnings/_asserting.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |
Methods:¤
__enter__ ¤
Enter the block, recording the warnings raised inside it.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
If this instance is already inside its block. |
Source code in src/frequenz/core/warnings/_asserting.py
__exit__ ¤
__exit__(
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None
Exit the block, replaying what it didn't reject and failing on the rest.
| PARAMETER | DESCRIPTION |
|---|---|
exc_type
|
The type of the exception leaving the block, if any.
TYPE:
|
exc_value
|
The exception leaving the block, if any.
TYPE:
|
traceback
|
The traceback of that exception, if any.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
AssertionError
|
If a matching warning was raised inside the block and no exception is on its way out, which is the more important news. |
Source code in src/frequenz/core/warnings/_asserting.py
__init__ ¤
Initialize this instance.
| PARAMETER | DESCRIPTION |
|---|---|
category
|
The category of warnings to reject, including its subclasses. |
message
|
A regular expression the start of the warning message must match, case insensitively. The default matches every message.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If any argument has the wrong type. |
error
|
If |
Source code in src/frequenz/core/warnings/_asserting.py
frequenz.core.warnings.ignoring_warnings ¤
Bases: AbstractContextManager[None]
A context manager that ignores the matching warnings raised inside its block.
The constructor arguments select which warnings are ignored, and mean the same as
the arguments of warnings.filterwarnings with action="ignore".
Unlike warnings.catch_warnings, this doesn't reset the warnings
deduplication history of the program, so warnings shown before the block are not
shown again after it. See the module documentation for the background.
Example
import warnings
from frequenz.core.warnings import ignoring_warnings
# Some third-party function warns about something out of our control.
def third_party() -> int:
warnings.warn("this is fine, actually", RuntimeWarning, stacklevel=2)
return 42
def use_third_party() -> int:
with ignoring_warnings(RuntimeWarning, message="this is fine"):
return third_party()
This is not a catch_warnings replacement
This only adds a filter for the duration of the block, it doesn't save and
restore the filters around it. Any change made to the filters inside the
block, by this thread or another one, is still in effect afterwards, exactly
as if the block wasn't there. Use warnings.catch_warnings when the
point is to undo filter changes.
Scope
The filter is added to the filters the interpreter consults, which are
normally shared by every thread and every asyncio task, so a matching
warning raised by unrelated code running concurrently is ignored too, for as
long as the block lasts. Keep the block around the call that needs it, and
prefer not to hold it across an await.
Wrapping this in a warnings.catch_warnings block only narrows that on
Python 3.14 and newer, with sys.flags.context_aware_warnings on, which is
off by default except in free-threaded builds. The outer block then gives
the current context a filters list of its own, kept in a
contextvars.ContextVar, so other threads, and tasks created before
it, are no longer affected. Tasks created inside it still inherit that same
list, so they are silenced too. Without the flag,
warnings.catch_warnings reaches for the same shared state and
isolates nothing. Either way it resets the deduplication history, which is
what this class exists to avoid.
Not reentrant
Entering an instance that is already inside its block raises
RuntimeError. A with ignoring_warnings(...) statement builds a new
instance each time, so this only comes up when one instance is kept in a
variable and entered again from inside itself, by a recursive function or a
helper called in the block. Nesting is rejected rather than supported
because the inner block would only ignore what the outer one already
ignores, so there is nothing to gain from making it work; an instance can
be entered again once it has been left.
Not thread-safe
Taking the filter out again is a lookup followed by a deletion, with no locking in between, so another thread removing a filter from the same list at the same time can make this delete the wrong one, dropping a filter that was not this block's and leaving this block's behind. Nothing guards against that, because a lock would cost every block to protect against something only a program that changes warning filters from several threads at once can do. If yours does, don't use this.
Source code in src/frequenz/core/warnings/_ignoring.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
Methods:¤
__enter__ ¤
Enter the block, adding the filter.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
If this instance is already inside its block. |
Source code in src/frequenz/core/warnings/_ignoring.py
__exit__ ¤
__exit__(
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None
Exit the block, taking the filter out again.
| PARAMETER | DESCRIPTION |
|---|---|
exc_type
|
The type of the exception leaving the block, if any.
TYPE:
|
exc_value
|
The exception leaving the block, if any.
TYPE:
|
traceback
|
The traceback of that exception, if any.
TYPE:
|
Source code in src/frequenz/core/warnings/_ignoring.py
__init__ ¤
Initialize this instance.
| PARAMETER | DESCRIPTION |
|---|---|
category
|
The category of warnings to ignore, including its subclasses. |
message
|
A regular expression the start of the warning message must match, case insensitively. The default matches every message.
TYPE:
|
module
|
A regular expression the start of the module name must match. The default matches every module.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If any argument has the wrong type. |
error
|
If |
Source code in src/frequenz/core/warnings/_ignoring.py
Functions:¤
frequenz.core.warnings.asserting_no_deprecations ¤
asserting_no_deprecations(
*, message: str = ""
) -> asserting_no_warnings
Fail if a deprecation warning is raised inside the block.
This is asserting_no_warnings for the usual case,
checking that the replacement for a deprecated symbol doesn't itself go through
the deprecated one.
All limitations of asserting_no_warnings() apply
Read the documentation of asserting_no_warnings,
many limitations apply here too, as this is only a thin wrapper around it.
Pass message when the code under test legitimately deprecates something else,
or reaches a third-party deprecation there is nothing to be done about: the block
wraps the code being tested, so unlike an
ignoring_deprecations block it can't be narrowed by
making it shorter.
Example
| PARAMETER | DESCRIPTION |
|---|---|
message
|
A regular expression the start of the warning message must match, case insensitively. The default rejects every deprecation.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
asserting_no_warnings
|
A context manager that fails if a deprecation warning is raised in it. |
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If |
error
|
If |
Source code in src/frequenz/core/warnings/_asserting.py
frequenz.core.warnings.deprecated_aliases ¤
deprecated_aliases(
module: str,
/,
*aliases: DeprecatedAlias,
category: type[Warning] = DeprecationWarning,
stacklevel: int = 2,
) -> Callable[[str], Any]
Build a module __getattr__ that warns about deprecated aliases.
Use this for symbols that moved to another module, or were renamed, but should
keep working from their old import path, when a typing_extensions.deprecated
decorator is not an option because the object is not yours to mark: decorating
it would deprecate it for everybody, including the users of its new home or name.
The alias also stays the very same object, so isinstance keeps working
through both paths.
Each DeprecatedAlias says when that alias was deprecated
or provides its own message, so aliases deprecated in different releases can
emit different warnings.
Danger
Follow the usage example structure strictly. In particular never drop
the else:, otherwise a type checker will see the __getattr__
assignment and treat every symbol in the module as Any,
effectively disabling type checking for the entire module.
Usage
This is how a module that used to define Decimal itself, and now gets it
from decimal, keeps the old import path working:
from typing import TYPE_CHECKING, TypeAlias
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
if TYPE_CHECKING:
# Private import, only for type checkers
from decimal import Decimal as _Decimal
Decimal: TypeAlias = _Decimal
else:
__getattr__ = deprecated_aliases(
__name__,
DeprecatedAlias("Decimal", new_module="decimal", since="v1.2.0"),
)
Reaching Decimal through this module, say mypkg.Decimal, now emits a
DeprecationWarning saying mypkg.Decimal is deprecated since v1.2.0. Use
decimal.Decimal instead.
Renaming
When the symbol was also renamed on the way out, new_name gives the name
it has now:
from typing import TYPE_CHECKING, TypeAlias
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
if TYPE_CHECKING:
from fractions import Fraction as _Fraction
Rational: TypeAlias = _Fraction
else:
__getattr__ = deprecated_aliases(
__name__,
DeprecatedAlias(
"Rational",
new_module="fractions",
new_name="Fraction",
since="v1.3.0",
),
)
Without new_module, the symbol was renamed in this very module:
from typing import TYPE_CHECKING, TypeAlias
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
class Widget:
... # Called Gadget before v1.4.0
if TYPE_CHECKING:
Gadget: TypeAlias = Widget
else:
__getattr__ = deprecated_aliases(
__name__,
DeprecatedAlias("Gadget", new_name="Widget", since="v1.4.0"),
)
Custom message
When the standard wording is not enough, an alias can give its whole
message instead of since, as a template that gets the fully qualified old
and new names as {old} and {new}:
from typing import TYPE_CHECKING, TypeAlias
from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases
if TYPE_CHECKING:
from decimal import Decimal as _Decimal
Decimal: TypeAlias = _Decimal
else:
__getattr__ = deprecated_aliases(
__name__,
DeprecatedAlias(
"Decimal",
new_module="decimal",
message="{old} is deprecated since v1.2.0 and will be removed "
"in v2.0.0. Use {new} instead.",
),
)
Deprecating whole modules
This function can't be used to deprecate a whole module. If you need to
do that, you can keep a real __init__.py at the old path, with a
__getattr__ = deprecated_aliases(...) that includes all the symbols
that used to be reachable there.
Security Warning
This function imports the modules the aliases point at, so the aliases are
as trusted as an import statement in this module. Write them out as
literals; don't build them from anything that comes from outside the
program.
| PARAMETER | DESCRIPTION |
|---|---|
module
|
The fully qualified name of the module defining the aliases.
TYPE:
|
*aliases
|
The deprecated aliases, at least one, each with its own name.
TYPE:
|
category
|
The category of the warning to emit.
TYPE:
|
stacklevel
|
How far up the stack the warning is reported, counting from the
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Callable[[str], Any]
|
A function suitable for use as the module's |
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If an argument has the wrong type, including an alias that is
not a |
ValueError
|
If there are no aliases, if two of them have the same name, if
one points at another alias in the same module, itself included, or if
|
Source code in src/frequenz/core/warnings/_deprecated_aliases.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | |
frequenz.core.warnings.ignoring_deprecations ¤
ignoring_deprecations(
*, message: str = "", module: str = ""
) -> ignoring_warnings
Ignore the deprecation warnings raised inside the block.
This is ignoring_warnings for the most common case,
library code that has to touch a symbol it deprecated itself. The user was
already warned by the deprecated symbol they used; warning them again from its
internals, about something they can do nothing about, is noise.
All limitations of ignoring_warnings() apply
Read the documentation of ignoring_warnings,
many limitations apply here too, as this is only a thin wrapper around it.
Tip
Wrap the call that reaches the deprecated symbol, not everything around it. A block that covers more than that also silences deprecations that have nothing to do with the one it was added for.
Example
import warnings
from frequenz.core.warnings import ignoring_deprecations
# A type that used to be public and is now deprecated.
class Wrapper:
def __init__(self, raw: str) -> None:
warnings.warn("Wrapper is deprecated", DeprecationWarning, stacklevel=2)
self.raw = raw
def from_wire(raw: str) -> Wrapper:
# Whatever else this function does, the deprecations raised out there
# are the user's business, so they stay outside the block.
with ignoring_deprecations():
return Wrapper(raw)
Inside a function that is itself deprecated
A deprecated function has already warned its caller about this code path, so the deprecations it reaches on the way are noise too. Even there, keep the block around the calls that raise them rather than putting it around the whole body, which this module can't do for you: it is a context manager and refuses to be used as a decorator.
import warnings
from typing_extensions import deprecated
from frequenz.core.warnings import ignoring_deprecations
# A type that used to be public and is now deprecated.
class Wrapper:
def __init__(self, raw: str) -> None:
warnings.warn("Wrapper is deprecated", DeprecationWarning, stacklevel=2)
self.raw = raw
async def save(wrapper: Wrapper) -> None:
print(f"saving {wrapper.raw}")
@deprecated("Use from_wire() instead")
async def parse(raw: str) -> Wrapper:
with ignoring_deprecations():
wrapper = Wrapper(raw)
await save(wrapper) # Outside: the scope note applies across an await.
return wrapper
| PARAMETER | DESCRIPTION |
|---|---|
message
|
A regular expression the start of the warning message must match, case insensitively. The default matches every deprecation, which is usually right: the way to be precise here is a short block, not a narrow filter. Use it when the call being wrapped can also raise a deprecation that should be heard.
TYPE:
|
module
|
A regular expression the start of the module name must match. The default matches every module.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ignoring_warnings
|
A context manager that ignores deprecation warnings. |
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If any argument has the wrong type. |
error
|
If |
Source code in src/frequenz/core/warnings/_ignoring.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |