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

CLN: @doc - base.py & indexing.py #31970

Merged
merged 21 commits into from
Mar 17, 2020
Merged

Conversation

HH-MWB
Copy link
Contributor

@HH-MWB HH-MWB commented Feb 14, 2020

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 14, 2020

Replacing all docstring decorator at once will be too hard to accomplish. I would prefer to do it step-by-step. Clean a few / one file in a PR, and do things iteratively.

Would you mind help me review the current changes? At the mean time, I will keep working on next part.

@HH-MWB HH-MWB requested a review from WillAyd February 14, 2020 12:36
@WillAyd
Copy link
Member

WillAyd commented Feb 14, 2020

@HH-MWB can you post screenshots of the affected docstrings?

Copy link
Member

@simonjayhawkins simonjayhawkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @HH-MWB generally lgtm, however a few questions.

@@ -1352,8 +1353,7 @@ def memory_usage(self, deep=False):
"""
return self._codes.nbytes + self.dtype.categories.memory_usage(deep=deep)

@Substitution(klass="Categorical")
@Appender(_shared_docs["searchsorted"])
@doc(IndexOpsMixin.searchsorted, klass="Categorical")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be inheriting from core.arrays.base.ExtensionArray instead? Also I can't see this docstring in the published docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this comment here - this seems strange to use IndexOpsMixin since it isn't part of this classes hierarchy. Any reason not to address this?

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 15, 2020

@HH-MWB can you post screenshots of the affected docstrings?

@WillAyd No problem. Should I use help() or .__doc__?

@WillAyd
Copy link
Member

WillAyd commented Feb 15, 2020

You can build the doc strings and just post a screenshot of the HTML

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 15, 2020

You can build the doc strings and just post a screenshot of the HTML

Get! Will do, thanks!

Copy link
Member

@simonjayhawkins simonjayhawkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @HH-MWB lgtm. would rather #31970 (comment) was addressed here but could be a follow-up.

would it be feasible to inherit the docstring template from the super class instead of explicitly specifying it? It would address my concern for Categorical array.

@simonjayhawkins simonjayhawkins added this to the 1.1 milestone Feb 15, 2020
@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 15, 2020

thanks @HH-MWB lgtm. would rather #31970 (comment) was addressed here but could be a follow-up.

would it be feasible to inherit the docstring template from the super class instead of explicitly specifying it? It would address my concern for Categorical array.

@simonjayhawkins Sorry for the late reply.

First, I agree with you that inherit the docstring from the super class is very intuitive, and also could saves us a lot of time. Then, when I dive deeper for this case, I found that the original docstring is different with it's super class. This difference confused me and I am trying to figure it out what cause this difference and how can we solve it.

Inherit Docstring

I think it is a very good idea to inherit the docstring. Maybe this should not be a default behavior but at least we should be able to do inheritance easily.

A potential quick solution for this is to create another decorator, for example, doc_inhert. I would expect it to bring the first non-empty docstring for it's super class.

For example, B.f should have the docstring inhert from A.f:

class A:
    def f(self):
        """func doc"""

class B(A):
    @doc_inhert
    def f(self):
        pass

Another example, C.f should have the docstring inhert from A.f, because B.f is empty:

class A:
    def f(self):
        """func doc"""

class B(A):
    def f(self):
        pass

class C(B):
    @doc_inhert
    def f(self):
        pass

I am still looking to see if there are other better solutions. For example, one thing in my mind will be create decorator for the class, and do docstring inhert for all methods belong to that class.

Would you mind if I create a new issue about docstring inheritance and propose my idea there? I am not 100% sure, but I think we already have some issues about this.

Open for suggestions and advices. What should I do next?

Case Here

As what I mentioned above, the original docstring is different from it's super class's docstring. Should I keep it as the original one or switch to be same as the super class one?

Here is the original docstring:

Find indices where elements should be inserted to maintain order.

Find the indices into a sorted {klass} `self` such that, if the
corresponding elements in `value` were inserted before the indices,
the order of `self` would be preserved.

.. note::

    The {klass} *must* be monotonically sorted, otherwise
    wrong locations will likely be returned. Pandas does *not*
    check this for you.

Parameters
----------
value : array_like
    Values to insert into `self`.
side : {{'left', 'right'}}, optional
    If 'left', the index of the first suitable location found is given.
    If 'right', return the last such index.  If there is no suitable
    index, return either 0 or N (where N is the length of `self`).
sorter : 1-D array_like, optional
    Optional array of integer indices that sort `self` into ascending
    order. They are typically the result of ``np.argsort``.

Returns
-------
int or array of int
    A scalar or array of insertion points with the
    same shape as `value`.

    .. versionchanged:: 0.24.0
        If `value` is a scalar, an int is now always returned.
        Previously, scalar inputs returned an 1-item array for
        :class:`Series` and :class:`Categorical`.

See Also
--------
sort_values
numpy.searchsorted

Notes
-----
Binary search is used to find the required insertion points.

Examples
--------
>>> x = pd.Series([1, 2, 3])
>>> x
0    1
1    2
2    3
dtype: int64

>>> x.searchsorted(4)
3

>>> x.searchsorted([0, 4])
array([0, 3])

>>> x.searchsorted([1, 3], side='left')
array([0, 2])

>>> x.searchsorted([1, 3], side='right')
array([1, 3])

>>> x = pd.Categorical(['apple', 'bread', 'bread',
                        'cheese', 'milk'], ordered=True)
[apple, bread, bread, cheese, milk]
Categories (4, object): [apple < bread < cheese < milk]

>>> x.searchsorted('bread')
1

>>> x.searchsorted(['bread'], side='right')
array([3])

If the values are not monotonically sorted, wrong locations
may be returned:

>>> x = pd.Series([2, 1, 3])
>>> x.searchsorted(1)
0  # wrong result, correct would be 1

Here is the docstring from it's super class:

Find indices where elements should be inserted to maintain order.

.. versionadded:: 0.24.0

Find the indices into a sorted array `self` (a) such that, if the
corresponding elements in `value` were inserted before the indices,
the order of `self` would be preserved.

Assuming that `self` is sorted:

======  ================================
`side`  returned index `i` satisfies
======  ================================
left    ``self[i-1] < value <= self[i]``
right   ``self[i-1] <= value < self[i]``
======  ================================

Parameters
----------
value : array_like
    Values to insert into `self`.
side : {'left', 'right'}, optional
    If 'left', the index of the first suitable location found is given.
    If 'right', return the last such index.  If there is no suitable
    index, return either 0 or N (where N is the length of `self`).
sorter : 1-D array_like, optional
    Optional array of integer indices that sort array a into ascending
    order. They are typically the result of argsort.

Returns
-------
array of ints
    Array of insertion points with the same shape as `value`.

See Also
--------
numpy.searchsorted : Similar method from NumPy.

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 15, 2020

You can build the doc strings and just post a screenshot of the HTML

Hi @WillAyd, I might mis-understand what you said.

I create HTML via python make.py html, but because all files changed is under pandas.core and pandas.util, the html doc might not cover those things.

The pandas.core, pandas.compat, and pandas.util top-level modules are PRIVATE. Stable functionality in such modules is not guaranteed.

@simonjayhawkins
Copy link
Member

Would you mind if I create a new issue about docstring inheritance and propose my idea there? I am not 100% sure, but I think we already have some issues about this.

Open for suggestions and advices. What should I do next?

Thanks for the detail. It looks like we should definitely have a separate issue/PR for this discussion.

@simonjayhawkins
Copy link
Member

I create HTML via python make.py html, but because all files changed is under pandas.core and pandas.util, the html doc might not cover those things.

am I correct in thinking the only published docstring affected by this PR is https://dev.pandas.io/docs/reference/api/pandas.Series.searchsorted.html? If so the screenshot of that should be OK.

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Feb 16, 2020

am I correct in thinking the only published docstring affected by this PR is https://dev.pandas.io/docs/reference/api/pandas.Series.searchsorted.html? If so the screenshot of that should be OK.

Thanks for your help @simonjayhawkins! Here is the screen shots.

Screen Shot 2020-02-16 at 2 40 57 AM

Screen Shot 2020-02-16 at 2 42 47 AM

@simonjayhawkins
Copy link
Member

The screenshots are from locally generated documentation?

@pep8speaks
Copy link

pep8speaks commented Feb 29, 2020

Hello @HH-MWB! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2020-03-11 23:21:34 UTC

@HH-MWB HH-MWB requested a review from datapythonista March 1, 2020 22:19
arg.format(**kwargs)
if isinstance(arg, str)
else dedent(arg.__doc__) # type: ignore
for arg in wrapper._doc_args # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you confirm that these # type: ignore stem from the fact that we are adding attributes to the wrapper function?

I guess this was discussed originally, but would using a class for the doc decorator help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point here! Most # type: ignore is because we are adding attributes to function.

The only one not for this reason is else dedent(arg.__doc__) # type: ignore (line 288). This one is because theoretically, arg.__doc__ can be str or None. However, we have checked arg.__doc__ is not None before putting it in, so that we are safe here.

Back to the most # type: ignore cases. I strongly agree with you that we could consider solving this issue without using # type: ignore, because this looks like an abuse. I would like to try putting it under a class. Thanks for this advice!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only one not for this reason is else dedent(arg.__doc__) # type: ignore (line 288). This one is because theoretically, arg.__doc__ can be str or None. However, we have checked arg.__doc__ is not None before putting it in, so that we are safe here.

i think ok to remove

$ mypy pandas --warn-unused-ignores
pandas\util\_decorators.py:288: error: unused 'type: ignore' comment

Copy link
Contributor Author

@HH-MWB HH-MWB Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simonjayhawkins Good catch here! Thanks. Fixing is here.

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Mar 3, 2020

Hello @simonjayhawkins. It seems very hard to remove all # type: ignore because we might have to bind a variable to the function instead of the decorator.

For use case of @doc(f, ...), we should be able to get original templates from function f. If there are any other way might work, please let me know, and I will be more than happy to give it a try.

However, we can reduce using # type: ignore from 6 times to 3 times.

Here is my proposal by using class:

class doc:
    """
    A decorator take docstring templates, concatenate them and perform string
    substitution on it.

    This decorator will add a variable "_docs" to the wrapped function to keep
    track the original docstring template for potential usage. If it should be
    consider as a template, it will be saved as a string. Otherwise, it will be
    saved as callable, and later user __doc__ and dedent to get docstring.

    Parameters
    ----------
    *args : str or callable
        The string / docstring / docstring template to be appended in order
        after default docstring under function.
    **kwags : str
        The string which would be used to format docstring template.
    """

    def __init__(self, *args: Union[str, Callable], **kwargs: str) -> None:
        self._docs: List[Union[str, Callable]] = []
        for arg in args:
            if hasattr(arg, "_docs"):
                self._docs.extend(arg._docs)  # type: ignore
            elif isinstance(arg, str) or arg.__doc__:
                self._docs.append(arg)

        self.parameters = kwargs

    def __call__(self, func: F) -> F:
        if func.__doc__:
            self._docs.insert(0, dedent(func.__doc__))

        func.__doc__ = "".join(
            [
                arg.format(**self.parameters)
                if isinstance(arg, str)
                else dedent(arg.__doc__)  # type: ignore
                for arg in self._docs
            ]
        )

        func._docs = self._docs  # type: ignore

        return func

We can use same idea to reduce # type: ignore from using function:

def doc(*args: Union[str, Callable], **kwargs: str) -> Callable[[F], F]:
    """
    A decorator take docstring templates, concatenate them and perform string
    substitution on it.

    This decorator will add a variable "_docs" to the wrapped function to keep
    track the original docstring template for potential usage. If it should be
    consider as a template, it will be saved as a string. Otherwise, it will be
    saved as callable, and later user __doc__ and dedent to get docstring.

    Parameters
    ----------
    *args : str or callable
        The string / docstring / docstring template to be appended in order
        after default docstring under function.
    **kwags : str
        The string which would be used to format docstring template.
    """

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapper(*args, **kwargs) -> Callable:
            return func(*args, **kwargs)

        # collecting docstring and docstring templates
        docs: List[Union[str, Callable]] = []
        if func.__doc__:
            docs.append(dedent(func.__doc__))

        for arg in args:
            if hasattr(arg, "_docs"):
                docs.extend(arg._docs)  # type: ignore
            elif isinstance(arg, str) or arg.__doc__:
                docs.append(arg)

        # formatting templates and concatenating docstring
        wrapper.__doc__ = "".join(
            [
                arg.format(**kwargs)
                if isinstance(arg, str)
                else dedent(arg.__doc__)  # type: ignore
                for arg in docs
            ]
        )

        # saving docstring templates list
        wrapper._docs = docs  # type: ignore

        return cast(F, wrapper)

    return decorator

Open for feed backs here. Which one is better? Any other improvement suggestions please?

@simonjayhawkins
Copy link
Member

simonjayhawkins commented Mar 3, 2020

Open for feed backs here. Which one is better? Any other improvement suggestions please?

probably best to leave as a follow-up and have more detailed discussion and just keep the changes to the decorator in this PR limited to the changes needed for using a docstring from a method that is not decorated.

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Mar 4, 2020

Open for feed backs here. Which one is better? Any other improvement suggestions please?

probably best to leave as a follow-up and have more detailed discussion and just keep the changes to the decorator in this PR limited to the changes needed for using a docstring from a method that is not decorated.

Sounds very reasonable. I will follow-up latter.

@@ -268,17 +269,24 @@ def decorator(func: F) -> F:
def wrapper(*args, **kwargs) -> Callable:
return func(*args, **kwargs)

templates = [func.__doc__ if func.__doc__ else ""]
# collecting and docstring templates
wrapper._doc_args: List[Union[str, Callable]] = [] # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name _doc_args is misleading. In the context of this function makes sense, since it's the value of the args parameters of the doc decorator, but if I see a _doc_args attribute of for example pandas.Series.head, I don't think it really tells what's in it. Something like _docstring_components could be clearer?

Also, when can it contain a callable? Even when we have something like @doc(pandas.Series.head) we're extending it with the content templates of pandas.Series.head, not with the function.

Last thing, what about using a docstring_components variable to avoid all the type: ignore, and just set wrapper._docstring_components = _docstring_components at the end? Probably cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@datapythonista Thanks for this suggestion, especially for the new variable name. I strongly agree with you here.

Copy link
Member

@datapythonista datapythonista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a trailing space and a mypy error, other than that looks good.

This decorator is robust even if func.__doc__ is None. This decorator will
add a variable "_docstr_template" to the wrapped function to save original
docstring template for potential usage.
This decorator will add a variable "_docstring_components" to the wrapped
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a trailing whitespace in this line breaking the CI.

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Mar 8, 2020

There is a trailing space and a mypy error, other than that looks good.

@datapythonista, @simonjayhawkins. Sorry to bother you. This mypy error makes me very confused.

In this commit, I removed type: ignore but everything works fine. However, now it shows the type error. I am confused why the original code can pass without error but the new code can't.

I know we can easily fix this by adding type: ignore, but I would like to know what case this problem and if there are a better way to solve that.

# formatting templates and concatenating docstring
wrapper.__doc__ = "".join(
[
arg.format(**kwargs) if isinstance(arg, str) else dedent(arg.__doc__)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator can be used like this:

@doc
def foo():
    pass

@doc(foo)
def bar():
    pass

If that happens, in this line arg will be the function foo, and arg.__doc__ will be None. When dedent is called with None as parameter we get:

>>> dedent(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mgarcia/miniconda3/lib/python3.7/textwrap.py", line 430, in dedent
    text = _whitespace_only_re.sub('', text)
TypeError: expected string or bytes-like object

And mypy is warning you about it. There are different ways of fixing this. May be this?

Suggested change
arg.format(**kwargs) if isinstance(arg, str) else dedent(arg.__doc__)
arg.format(**kwargs) if isinstance(arg, str) else dedent(arg.__doc__ or "")

Copy link
Contributor Author

@HH-MWB HH-MWB Mar 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! That make sense.

In line 281 and 282, there are codes to make sure arg.__doc__ won't be None.

elif isinstance(arg, str) or arg.__doc__:
    docstring_components.append(arg)

In this case, maybe we can use # type: ignore?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that code ensures _docstring_components is not set to None (in the parent). And the exception won't happen as far as _docstring_components is always set with the @doc decorator. But technically speaking there is no guarantee that something like this can be done:

def foo():
    pass
foo._docstring_components = [None]

@doc(foo)
def bar():
    pass

So, my preference is an actual fix, like the one I proposed, than a typing ignore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that code ensures _docstring_components is not set to None (in the parent). And the exception won't happen as far as _docstring_components is always set with the @doc decorator. But technically speaking there is no guarantee that something like this can be done:

def foo():
    pass
foo._docstring_components = [None]

@doc(foo)
def bar():
    pass

So, my preference is an actual fix, like the one I proposed, than a typing ignore.

That is a good point! Error fixed. Thanks!

]
)

# saving docstring components list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not worth having this comment.

Copy link
Member

@datapythonista datapythonista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just one idea that could make sense.

Thanks for the work on this @HH-MWB, it's a great improvement.

Comment on lines +279 to +280
if hasattr(arg, "_docstring_components"):
docstring_components.extend(arg._docstring_components) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this could be a better fix for this one:

Suggested change
if hasattr(arg, "_docstring_components"):
docstring_components.extend(arg._docstring_components) # type: ignore
if hasattr(arg, "_docstring_components") and isinstance(arg._docstring_components, list):
docstring_components.extend(arg._docstring_components)

Copy link
Contributor Author

@HH-MWB HH-MWB Mar 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @datapythonista. I would consider this as two parts. One is to add type check for _docstring_components and one for remove # type: ignore.

For remove # type: ignore

After I made the change, I still get the errors below and not sure if we can remove that.

pandas/util/_decorators.py:280: error: Item "str" of "Union[str, Callable[..., Any]]" has no attribute "_docstring_components"
pandas/util/_decorators.py:280: error: Item "function" of "Union[str, Callable[..., Any]]" has no attribute "_docstring_components"

I guess my settings are not the same as checks here. I even the last passed build, I still get mypy errors on local. So, the change might fix it and just I don't know.

For checking if _docstring_components is list

I think this is a very good point, we do as defensive programming. But I am a little bit curious about how far we should go to protect it. Also, since this function is internal use for pandas only, would it be better to let it failed? Then the developer would get notified there is a conflict immediately?

Just to be clarified, I would be happy to make this change if you (or anyone) feels needed. Because I see both pros and cons for this, I am not sure which one will be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to merge this as is, was wondering if the suggested change would fix the problem.

We can surely go back to it in the future if needed, but let's merge this first, since this already adds a lot of value, and we can start replacing Appender by doc at a larger scale after this.

@@ -51,7 +52,7 @@
_extension_array_shared_docs,
try_cast_to_ea,
)
from pandas.core.base import NoNewAttributesMixin, PandasObject, _shared_docs
from pandas.core.base import IndexOpsMixin, NoNewAttributesMixin, PandasObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I am in the minority on this view and don't want to be overly difficult, but can you refactor the IndexOpsMixin as a pre-cursor to this, or leave this module separate from the rest of changes (which look good btw)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is a good idea. I would convert this back to using _shared_docs. This PR is already too large, and I should really put them separately.

Copy link
Member

@datapythonista datapythonista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you merge master. I guess the CI problems are unrelated.

Comment on lines +279 to +280
if hasattr(arg, "_docstring_components"):
docstring_components.extend(arg._docstring_components) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to merge this as is, was wondering if the suggested change would fix the problem.

We can surely go back to it in the future if needed, but let's merge this first, since this already adds a lot of value, and we can start replacing Appender by doc at a larger scale after this.

@HH-MWB HH-MWB force-pushed the use-doc-decorator branch from c2c4931 to 95a86ee Compare March 11, 2020 23:21
@HH-MWB HH-MWB requested a review from WillAyd March 12, 2020 02:03
@WillAyd WillAyd merged commit 871e3af into pandas-dev:master Mar 17, 2020
@WillAyd
Copy link
Member

WillAyd commented Mar 17, 2020

Great thanks @HH-MWB ! If you can do a follow up to fix the IndexOpsMixin import would be very much appreciated!

@HH-MWB
Copy link
Contributor Author

HH-MWB commented Mar 17, 2020

Great thanks @HH-MWB ! If you can do a follow up to fix the IndexOpsMixin import would be very much appreciated!

Yes. I am thinking what is the best way of doing that. Definitely want to keep making contributing on this! Thanks.

SeeminSyed pushed a commit to CSCD01-team01/pandas that referenced this pull request Mar 22, 2020
@HH-MWB HH-MWB deleted the use-doc-decorator branch March 23, 2020 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants