Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Luca fixtures doc #934

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 107 additions & 29 deletions docs/chapter-06.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Here is an example of usage:
@action.uses(T)
def index(): return str(T('Hello world'))

The string `hello world` will be translated based on the
The string ``hello world`` will be translated based on the
internationalization file in the specified “translations” folder that
best matches the HTTP ``accept-language`` header.

Expand Down Expand Up @@ -215,7 +215,7 @@ action with a counter that counts “visits”.
return str(T("You have been here {n} times").format(n=counter))


If the `T` fixture is to be used from inside a template you may want to pass it to the template:
If the ``T`` fixture is to be used from inside a template you may want to pass it to the template:

.. code:: python

Expand Down Expand Up @@ -311,7 +311,7 @@ you have to redefine the select method for the T instance:
T.on_request = lambda *_: T.local.__dict__.update(tag="it", language=T.languages["it"])

This is to be done outside any action and will apply to all actions. Action will still need to declare
`action.uses(T)` else the behavior is undefined.
``action.uses(T)`` else the behavior is undefined.



Expand Down Expand Up @@ -623,10 +623,10 @@ given condition. For example to enforce a workflow:
return locals()

Notice that the Condition fixtures takes a function as first argument
which is called `on_request` and must evaluate to True or False.
which is called ``on_request`` and must evaluate to True or False.

Also notice that in the above example the Condition depends on the Session
therefore it must be listed after `session` in `action.uses`.
therefore it must be listed after ``session`` in ``action.uses``.

If False, by default, the Condition fixture raises 404.
It is possible to specify a different exception:
Expand All @@ -643,7 +643,7 @@ for example, to redirect to another page:
Condition(cond, on_false=lambda: redirect(URL('step1')))

You can use condition to check permissions. For example, if you
are giving group memberships to users using `Tags` (it will be explained
are giving group memberships to users using ``Tags`` (it will be explained
later on the :ref:`Authorization using Tags` chapter), then you can
require that users action have specific group membership:

Expand Down Expand Up @@ -855,13 +855,13 @@ A fixture is an object with the following minimal structure:
def on_success(self, context): pass
def on_error(self, context) pass

For example in the DAL fixture case, `on_request` starts a transaction,
`on_success` commits it, and `on_error` rolls it back.
For example in the DAL fixture case, ``on_request`` starts a transaction,
``on_success`` commits it, and ``on_error`` rolls it back.

In the case of a template, `on_request` and `on_error` do nothing but
`on_success` transforms the output.
In the case of a template, ``on_request`` and ``on_error`` do nothing but
``on_success`` transforms the output.

In the case of `auth.user` fixtures, `on_request` does all the work of
In the case of ``auth.user`` fixtures, ``on_request`` does all the work of
determining if the user is logged in (from the dependent session fixture)
and eventually preventing the request from accessing the inner layers.

Expand All @@ -873,24 +873,24 @@ Under normal circumstances above methods are executed in this order:
request -> A.on_request -> B.on_request -> C.on_request -> action
response <- A.on_success <- B.on_success <- C.on_success <-

i.e. the first fixture (A) is the first one to call `on_request`
and the last one to call `on_success`. You can think of them as layers of
an onion with the action (user code) at the center. `on_success` is called
when entering a layer from the outside and `on_success` is called when
i.e. the first fixture (A) is the first one to call ``on_request``
and the last one to call ``on_success``. You can think of them as layers of
an onion with the action (user code) at the center. ``on_success`` is called
when entering a layer from the outside and ``on_success`` is called when
exiting a layer from the inside (like WSGI middleware).

If any point an exception is raised inner layers are not called
and outer layers will call `on_error` instead of `on_success`.
and outer layers will call ``on_error`` instead of ``on_success``.

Context is a shared object which contains:

- content['fixtures']: the list of all the fixtures for the action.
- context['processed']: the list of fixtures that called `on_request` previously within the request.
- context['processed']: the list of fixtures that called ``on_request`` previously within the request.
- context['exception']: the exception raised by the action or any previous fixture logic (usually None)
- context['output']: the action output.

`on_success` and `on_error` can see the current `context['exception']` and
transform it. They can see the current `context['output']` and transform it as well.
``on_success`` and ``on_error`` can see the current ``context['exception']`` and
transform it. They can see the current ``context['output']`` and transform it as well.

For example here is a fixture that transforms the output text to upper case:

Expand All @@ -906,7 +906,7 @@ For example here is a fixture that transforms the output text to upper case:
@action.uses(upper_case)
def index(): return "hello world"

Notice that this fixture assumes the `context['output']` is a string
Notice that this fixture assumes the ``context['output']`` is a string
and therefore it must come before the template.

Here is a fixture that logs exceptions tracebacks to a file:
Expand All @@ -926,14 +926,14 @@ Here is a fixture that logs exceptions tracebacks to a file:
@action.uses(errlog)
def index(): return 1/0

Fixtures also have a `__prerequisite__` attribute. If a fixture
Fixtures also have a ``__prerequisite__`` attribute. If a fixture
takes another fixture as an argument, its value must be appended
to the list of `__prerequisites__`. This guarantees that they are
to the list of ``__prerequisites__``. This guarantees that they are
always executed in the proper order even if listed in the wrong order.
It also makes it optional to declare prerequisite fixtures in `action.uses`.
It also makes it optional to declare prerequisite fixtures in ``action.uses``.

For example `Auth` depends on `db`, `session`, and `flash`. `db` and `session`
are indeed arguments. `flash` is a special singleton fixture declared within `Auth`.
For example ``Auth`` depends on ``db``, ``session``, and ``flash``. ``db`` and ``session``
are indeed arguments. ``flash`` is a special singleton fixture declared within ``Auth``.
This means that

.. code:: python
Expand All @@ -949,7 +949,7 @@ is equivalent to
Why are fixtures not simply functions that contain a try/except?

We considered the option but there are some special exceptions that should
not be considered errors but success (`py4web.HTTP`, `bottle.HTTResponse`)
not be considered errors but success (``py4web.HTTP``, ``bottle.HTTResponse``)
while other exceptions are errors. The actual logic can be complicated
and individual fixtures do not need to know these details.

Expand All @@ -961,6 +961,84 @@ Fixtures should not in general communicate with each other but nothing
prevents one fixture to put data in the context and another fixture to
retrieve that data.

Fixtures with dependencies
~~~~~~~~~~~~~~~~~~~~~~~~~~

If a fixture depends on another fixture, it needs to be passed that fixture in the initializer,
and the fixture must be listed in the ``__prerequisites__`` attribute.
For example, suppose we want to create a fixture that grants access to a controller only
to users whose email address is included in an ADMIN_EMAILS list.
We can write the following fixture:

.. code:: python

class AdminAccess(Fixture):

def __init__(self, auth, admin_list, redirect_url=None):
super().__init__()
self.admin_list = admin_list
self.auth = auth
self.__prerequisites__ = [auth]
# One thing to note here is that the URL function can only be called in a
# request context (while serving a request). Thus, we cannot store in the fixture
# initialization the full URL to redirect, but only the path.
self.redirect_url = redirect_url or 'index'

def on_request(self, context):
if ((not self.auth.current_user)
or self.auth.current_user.get('email') not in self.admin_list):
redirect(URL(self.redirect_url))

def on_error(self, context):
redirect(URL(self.redirect_url))

The fixture can be created and used as follows:

.. code:: python

admin_access = AdminAccess(auth, ['a@example.com',], 'index')

@action('/admin-only')
@action.uses('admin_only.html', admin_access)
def admin_only():
return dict()

Using local storage
~~~~~~~~~~~~~~~~~~~

Fixtures can use a thread-local storage for data they need.
Here is an example:

.. code:: python

class LocalStorageDemo(Fixture):

def __init__(self):
super().__init__()

def on_request(self, context):
Fixture.local_initialize(self)
# We can check whether the local storage is valid.
print(f"is_valid: {self.is_valid()}")
content = str(uuid.uuid4())
print(f"Storing content: {content}")
self.local.my_content = content

def on_success(self, context):
# The line below is used only to show that the thread-local object is in place.
print(f"Retrieved: {self.local.my_content}")

Notably, the initializer should contain the line:

.. code:: python

Fixture.local_initialize(self)

in order to initialize the thread-local storage.
Once this is done, the thread-local storage can be used to store and retrieve data
using the the ``self.local`` object.


Multiple fixtures
-----------------

Expand Down Expand Up @@ -1004,11 +1082,11 @@ is almost equivalent to

@action.uses(A,B)

but not quite. All fixtures declared in one `action.uses` share
the same context while fixtures in different `action.uses` use
but not quite. All fixtures declared in one ``action.uses`` share
the same context while fixtures in different ``action.uses`` use
different contexts and therefore they cannot communicate with each other.
This may change in the future.
For now we recommend using a single call to `action.uses`.
For now we recommend using a single call to ``action.uses``.


Caching and Memoize
Expand Down
Loading