From db29ad77574f195387aa0d07f3fec5f04c2decc2 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Thu, 5 Oct 2023 23:32:31 -0700 Subject: [PATCH] docs: fix docstring formatting, other tweaks --- activitypub.py | 82 +++++++++++---------- atproto.py | 25 +++---- common.py | 55 ++++++++------- convert.py | 6 +- docs/build.sh | 4 ++ docs/conf.py | 17 ++--- docs/source/modules.rst | 13 ++++ follow.py | 6 +- ids.py | 8 +-- models.py | 145 ++++++++++++++++++++------------------ pages.py | 19 +++-- protocol.py | 113 +++++++++++++++-------------- redirect.py | 24 +++---- tests/test_activitypub.py | 1 - tests/test_models.py | 1 - tests/test_web.py | 1 - web.py | 26 +++---- webfinger.py | 8 +-- 18 files changed, 292 insertions(+), 262 deletions(-) diff --git a/activitypub.py b/activitypub.py index 20ddee8..2d13f65 100644 --- a/activitypub.py +++ b/activitypub.py @@ -67,7 +67,7 @@ class ActivityPub(User, Protocol): """Validate id, require URL, don't allow Bridgy Fed domains. TODO: normalize scheme and domain to lower case. Add that to - :class:`util.UrlCanonicalizer`? + :class:`oauth_dropins.webutil.util.UrlCanonicalizer`\? """ super()._pre_put_hook() id = self.key.id() @@ -149,7 +149,7 @@ class ActivityPub(User, Protocol): @classmethod def target_for(cls, obj, shared=False): - """Returns `obj`'s or its author's/actor's inbox, if available.""" + """Returns ``obj``'s or its author's/actor's inbox, if available.""" # TODO: we have entities in prod that fail this, eg # https://indieweb.social/users/bismark has source_protocol webmention # assert obj.source_protocol in (cls.LABEL, cls.ABBREV, 'ui', None), str(obj) @@ -189,8 +189,8 @@ class ActivityPub(User, Protocol): def send(to_cls, obj, url, log_data=True): """Delivers an activity to an inbox URL. - If `obj.recipient_obj` is set, it's interpreted as the receiving actor - who we're delivering to and its id is populated into `cc`. + If ``obj.recipient_obj`` is set, it's interpreted as the receiving actor + who we're delivering to and its id is populated into ``cc``. """ if to_cls.is_blocklisted(url): logger.info(f'Skipping sending to {url}') @@ -213,47 +213,48 @@ class ActivityPub(User, Protocol): def fetch(cls, obj, **kwargs): """Tries to fetch an AS2 object. - Assumes obj.id is a URL. Any fragment at the end is stripped before + Assumes ``obj.id`` is a URL. Any fragment at the end is stripped before loading. This is currently underspecified and somewhat inconsistent across AP implementations: - https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/11 - https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/23 - https://socialhub.activitypub.rocks/t/s2s-create-activity/1647/5 - https://github.com/mastodon/mastodon/issues/13879 (open!) - https://github.com/w3c/activitypub/issues/224 + * https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/11 + * https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/23 + * https://socialhub.activitypub.rocks/t/s2s-create-activity/1647/5 + * https://github.com/mastodon/mastodon/issues/13879 (open!) + * https://github.com/w3c/activitypub/issues/224 - Uses HTTP content negotiation via the Content-Type header. If the url is - HTML and it has a rel-alternate link with an AS2 content type, fetches and - returns that URL. + Uses HTTP content negotiation via the ``Content-Type`` header. If the + url is HTML and it has a ``rel-alternate`` link with an AS2 content + type, fetches and returns that URL. Includes an HTTP Signature with the request. - https://w3c.github.io/activitypub/#authorization - https://tools.ietf.org/html/draft-cavage-http-signatures-07 - https://github.com/mastodon/mastodon/pull/11269 - Mastodon requires this signature if AUTHORIZED_FETCH aka secure mode is on: - https://docs.joinmastodon.org/admin/config/#authorized_fetch + * https://w3c.github.io/activitypub/#authorization + * https://tools.ietf.org/html/draft-cavage-http-signatures-07 + * https://github.com/mastodon/mastodon/pull/11269 + + Mastodon requires this signature if ``AUTHORIZED_FETCH`` aka secure mode + is on: https://docs.joinmastodon.org/admin/config/#authorized_fetch Signs the request with the current user's key. If not provided, defaults to using @snarfed.org@snarfed.org's key. - See :meth:`Protocol.fetch` for more details. + See :meth:`protocol.Protocol.fetch` for more details. Args: - obj: :class:`Object` with the id to fetch. Fills data into the as2 + obj (models.Object): with the id to fetch. Fills data into the as2 property. kwargs: ignored Returns: - True if the object was fetched and populated successfully, + bool: True if the object was fetched and populated successfully, False otherwise Raises: - :class:`requests.HTTPError`, :class:`werkzeug.exceptions.HTTPException` - - If we raise a werkzeug HTTPException, it will have an additional - requests_response attribute with the last requests.Response we received. + requests.HTTPError: + werkzeug.exceptions.HTTPException: will have an additional + ``requests_response`` attribute with the last + :class:`requests.Response` we received. """ url = obj.key.id() if not util.is_web(url): @@ -329,7 +330,7 @@ class ActivityPub(User, Protocol): """Verifies the current request's HTTP Signature. Args: - activity: dict, AS2 activity + activity (dict): AS2 activity Logs details of the result. Raises :class:`werkzeug.HTTPError` if the signature is missing or invalid, otherwise does nothing and returns None. @@ -417,19 +418,20 @@ def signed_post(url, **kwargs): def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs): - """Wraps requests.* and adds HTTP Signature. + """Wraps ``requests.*`` and adds HTTP Signature. - If the current session has a user (ie in g.user), signs with that user's + If the current session has a user (ie in ``g.user``), signs with that user's key. Otherwise, uses the default user snarfed.org. Args: - fn: :func:`util.requests_get` or :func:`util.requests_get` - url: str - data: optional AS2 object - log_data: boolean, whether to log full data object + fn (callable): :func:`util.requests_get` or :func:`util.requests_get` + url (str): + data (dict): optional AS2 object + log_data (bool): whether to log full data object kwargs: passed through to requests - Returns: :class:`requests.Response` + Returns: + requests.Response: """ if headers is None: headers = {} @@ -490,13 +492,15 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs): def postprocess_as2(activity, orig_obj=None, wrap=True): """Prepare an AS2 object to be served or sent via ActivityPub. - g.user is required. Populates it into the actor.id and publicKey fields. + ``g.user`` is required. Populates it into the ``actor.id`` and ``publicKey`` + fields. Args: - activity: dict, AS2 object or activity - orig_obj: dict, AS2 object, optional. The target of activity's inReplyTo or + activity (dict): AS2 object or activity + orig_obj (dict): AS2 object, optional. The target of activity's inReplyTo or Like/Announce/etc object, if any. - wrap: boolean, whether to wrap id, url, object, actor, and attributedTo + wrap (bool): whether to wrap id, url, object, actor, and attributedTo + """ if not activity or isinstance(activity, str): return activity @@ -650,8 +654,8 @@ def postprocess_as2_actor(actor, wrap=True): Modifies actor in place. Args: - actor: dict, AS2 actor object - wrap: boolean, whether to wrap url + actor (dict): AS2 actor object + wrap (bool): whether to wrap url Returns: actor dict diff --git a/atproto.py b/atproto.py index b03e9a9..a5ff8d3 100644 --- a/atproto.py +++ b/atproto.py @@ -122,10 +122,10 @@ class ATProto(User, Protocol): returning Bridgy Fed's URL as the PDS. Args: - obj: :class:`Object` + obj (Object) Returns: - str + str: """ id = obj.key.id() if id.startswith('did:'): @@ -169,10 +169,10 @@ class ATProto(User, Protocol): def _pds_for(cls, did_obj): """ Args: - did_obj: :class:`Object` + did_obj (Object) Returns: - str, PDS URL, or None + str: PDS URL, or None """ assert did_obj.key.id().startswith('did:') @@ -195,7 +195,7 @@ class ATProto(User, Protocol): """Creates an ATProto user, repo, and profile for a non-ATProto user. Args: - user (User) + user (models.User) """ assert not isinstance(user, ATProto) @@ -321,12 +321,12 @@ class ATProto(User, Protocol): """Tries to fetch a ATProto object. Args: - obj: :class:`Object` with the id to fetch. Fills data into the as2 + obj (models.Object): with the id to fetch. Fills data into the ``as2`` property. kwargs: ignored Returns: - True if the object was fetched and populated successfully, + bool: True if the object was fetched and populated successfully, False otherwise Raises: @@ -364,12 +364,13 @@ class ATProto(User, Protocol): @classmethod def serve(cls, obj): - """Serves an :class:`Object` as AS2. + """Serves an :class:`models.Object` as AS2. - This is minimally implemented to serve app.bsky.* lexicon data, but + This is minimally implemented to serve ``app.bsky.*`` lexicon data, but BGSes and other clients will generally receive ATProto commits via - `com.atproto.sync.subscribeRepos` subscriptions, not BF-specific - /convert/... HTTP requests, so this should never be used in practice. + ``com.atproto.sync.subscribeRepos`` subscriptions, not BF-specific + ``/convert/...`` HTTP requests, so this should never be used in + practice. """ return bluesky.from_as1(obj.as1), {'Content-Type': 'application/json'} @@ -378,7 +379,7 @@ class ATProto(User, Protocol): def poll_notifications(): """Fetches and enqueueus new activities from the AppView for our users. - Uses the `listNotifications` endpoint, which is intended for end users. 🤷 + Uses the ``listNotifications`` endpoint, which is intended for end users. 🤷 https://github.com/bluesky-social/atproto/discussions/1538 """ diff --git a/common.py b/common.py index 2cf32dc..b321f60 100644 --- a/common.py +++ b/common.py @@ -1,6 +1,4 @@ -# coding=utf-8 -"""Misc common utilities. -""" +"""Misc common utilities.""" import base64 from datetime import timedelta import logging @@ -75,18 +73,18 @@ TASKS_LOCATION = 'us-central1' def base64_to_long(x): - """Converts x from URL safe base64 encoding to a long integer. + """Converts from URL safe base64 encoding to long integer. - Originally from django_salmon.magicsigs. Used in :meth:`User.public_pem` + Originally from ``django_salmon.magicsigs``. Used in :meth:`User.public_pem` and :meth:`User.private_pem`. """ return number.bytes_to_long(base64.urlsafe_b64decode(x)) def long_to_base64(x): - """Converts x from a long integer to base64 URL safe encoding. + """Converts from long integer to base64 URL safe encoding. - Originally from django_salmon.magicsigs. Used in :meth:`User.get_or_create`. + Originally from ``django_salmon.magicsigs``. Used in :meth:`User.get_or_create`. """ return base64.urlsafe_b64encode(number.long_to_bytes(x)) @@ -103,22 +101,22 @@ def host_url(path_query=None): def error(msg, status=400, exc_info=None, **kwargs): - """Like flask_util.error, but wraps body in JSON.""" + """Like :func:`oauth_dropins.webutil.flask_util.error`, but wraps body in JSON.""" logger.info(f'Returning {status}: {msg}', exc_info=exc_info) abort(status, response=make_response({'error': msg}, status), **kwargs) def pretty_link(url, text=None, **kwargs): - """Wrapper around util.pretty_link() that converts Mastodon user URLs to @-@. + """Wrapper around :func:`oauth_dropins.webutil.util.pretty_link` that converts Mastodon user URLs to @-@ handles. Eg for URLs like https://mastodon.social/@foo and https://mastodon.social/users/foo, defaults text to @foo@mastodon.social if it's not provided. Args: - url: str - text: str - kwargs: passed through to :func:`webutil.util.pretty_link` + url (str) + text (str) + kwargs: passed through to :func:`oauth_dropins.webutil.util.pretty_link` """ if g.user and g.user.is_web_url(url): return g.user.user_page_link() @@ -144,12 +142,13 @@ def redirect_wrap(url): ...to satisfy Mastodon's non-standard domain matching requirement. :( Args: - url: string + url (str) * https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599 * https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747 - Returns: string, redirect url + Returns: + str: redirect url """ if not url or util.domain_from_link(url) in DOMAINS: return url @@ -160,15 +159,16 @@ def redirect_wrap(url): def redirect_unwrap(val): """Removes our redirect wrapping from a URL, if it's there. - val may be a string, dict, or list. dicts and lists are unwrapped + ``val`` may be a string, dict, or list. dicts and lists are unwrapped recursively. Strings that aren't wrapped URLs are left unchanged. Args: - val: string or dict or list + val (str or dict or list) - Returns: string, unwrapped url + Returns: + str: unwrapped url """ if isinstance(val, dict): return {k: redirect_unwrap(v) for k, v in val.items()} @@ -196,15 +196,16 @@ def redirect_unwrap(val): def webmention_endpoint_cache_key(url): """Returns cache key for a cached webmention endpoint for a given URL. - Just the domain by default. If the URL is the home page, ie path is / , the - key includes a / at the end, so that we cache webmention endpoints for home - pages separate from other pages. https://github.com/snarfed/bridgy/issues/701 + Just the domain by default. If the URL is the home page, ie path is ``/``, + the key includes a ``/`` at the end, so that we cache webmention endpoints + for home pages separate from other pages. + https://github.com/snarfed/bridgy/issues/701 - Example: 'snarfed.org /' + Example: ``snarfed.org /`` https://github.com/snarfed/bridgy-fed/issues/423 - Adapted from bridgy/util.py. + Adapted from ``bridgy/util.py``. """ parsed = urllib.parse.urlparse(url) key = parsed.netloc @@ -225,7 +226,7 @@ def webmention_discover(url, **kwargs): def add(seq, val): - """Appends val to seq if seq doesn't already contain it. + """Appends ``val`` to ``seq`` if seq doesn't already contain it. Useful for treating repeated ndb properties like sets instead of lists. """ @@ -240,13 +241,13 @@ def create_task(queue, **params): creating a task. Args: - queue: string, queue name + queue (str): queue name params: form-encoded and included in the task request body Returns: - :flask:`Response` from running the task inline if running in a local - server, otherwise (str response body, int status code) response from - creating the task. + flask.Response or (str, int): response from either running the task + inline, if running in a local server, or the response from creating the + task. """ assert queue path = f'/queue/{queue}' diff --git a/convert.py b/convert.py index f4b0c1d..6d83376 100644 --- a/convert.py +++ b/convert.py @@ -1,7 +1,7 @@ -"""Serves /convert/... URLs to convert data from one protocol to another. +"""Serves ``/convert/...`` URLs to convert data from one protocol to another. -URL pattern is /convert/SOURCE/DEST , where SOURCE and DEST are the LABEL -constants from the :class:`Protocol` subclasses. +URL pattern is ``/convert/SOURCE/DEST``, where ``SOURCE`` and ``DEST`` are the +``LABEL`` constants from the :class:`protocol.Protocol` subclasses. """ import logging import re diff --git a/docs/build.sh b/docs/build.sh index 4ab9f21..3e66675 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -28,4 +28,8 @@ source ../local/bin/activate # Run sphinx in the virtualenv's python interpreter so it can import packages # installed in the virtualenv. +# +# If sphinx crashes with eg: +# exception: '<' not supported between instances of 'dict' and 'dict' +# ...try running with -E to clear its cache. python3 `which sphinx-build` -b html . _build/html diff --git a/docs/conf.py b/docs/conf.py index a8e492a..37c9532 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -342,17 +342,18 @@ texinfo_documents = [ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'arroba': ('https://arroba.readthedocs.io/en/latest', None), - 'dag_cbor': ('https://dag-cbor.readthedocs.io/en/latest', None), + 'arroba': ('https://arroba.readthedocs.io/en/stable', None), + 'dag_cbor': ('https://dag-cbor.readthedocs.io/en/stable', None), 'flask': ('https://flask.palletsprojects.com/en/latest', None), 'flask_caching': ('https://flask-caching.readthedocs.io/en/latest', None), - 'granary': ('https://granary.readthedocs.io/en/latest', None), - 'multiformats': ('https://multiformats.readthedocs.io/en/latest', None), - 'oauth_dropins': ('https://oauth-dropins.readthedocs.io/en/latest', None), + 'granary': ('https://granary.readthedocs.io/en/stable', None), + 'lexrpc': ('https://granary.readthedocs.io/en/stable', None), + 'multiformats': ('https://multiformats.readthedocs.io/en/stable', None), + 'oauth_dropins': ('https://oauth-dropins.readthedocs.io/en/stable', None), 'python': ('https://docs.python.org/3/', None), - 'requests': ('https://requests.readthedocs.io/en/stable/', None), - 'urllib3': ('https://urllib3.readthedocs.io/en/latest', None), - 'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest/', None), + 'requests': ('https://requests.readthedocs.io/en/stable', None), + 'urllib3': ('https://urllib3.readthedocs.io/en/stable', None), + 'werkzeug': ('https://werkzeug.palletsprojects.com/en/latest', None), } # -- Post process ------------------------------------------------------------ diff --git a/docs/source/modules.rst b/docs/source/modules.rst index d1ccdb5..b6f15af 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -8,51 +8,64 @@ Reference documentation. activitypub ----------- .. automodule:: activitypub + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ atproto ------- .. automodule:: atproto + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ common ------ .. automodule:: common + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ convert ------- .. automodule:: convert + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ follow ------ .. automodule:: follow + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ models ------ .. automodule:: models + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ pages ----- .. automodule:: pages + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ protocol -------- .. automodule:: protocol + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ redirect -------- .. automodule:: redirect + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ render ------ .. automodule:: render + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ superfeedr ---------- .. automodule:: superfeedr + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ web --- .. automodule:: web + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ webfinger --------- .. automodule:: webfinger + :exclude-members: __eq__, __getnewargs__, __getstate__, __hash__, __new__, __repr__, __str__, __weakref__ diff --git a/follow.py b/follow.py index 8d60458..e573c79 100644 --- a/follow.py +++ b/follow.py @@ -1,8 +1,8 @@ """Remote follow handler. -https://github.com/snarfed/bridgy-fed/issues/60 -https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020 -https://www.rfc-editor.org/rfc/rfc7033 +* https://github.com/snarfed/bridgy-fed/issues/60 +* https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020 +* https://www.rfc-editor.org/rfc/rfc7033 """ import logging diff --git a/ids.py b/ids.py index dc78849..b4b6312 100644 --- a/ids.py +++ b/ids.py @@ -12,8 +12,8 @@ def convert_id(*, id, from_proto, to_proto): Args: id (str) - from_proto (Protocol) - to_proto (Protocol) + from_proto (protocol.Protocol) + to_proto (protocol.Protocol) Returns: str: the corresponding id in ``to_proto`` @@ -49,8 +49,8 @@ def convert_handle(*, handle, from_proto, to_proto): Args: handle (str) - from_proto (Protocol) - to_proto (Protocol) + from_proto (protocol.Protocol) + to_proto (protocol.Protocol) Returns: str: the corresponding handle in ``to_proto`` diff --git a/models.py b/models.py index b4b4310..176cde4 100644 --- a/models.py +++ b/models.py @@ -51,20 +51,22 @@ logger = logging.getLogger(__name__) class Target(ndb.Model): - """Protocol + URI pairs for identifying objects. + """:class:`protocol.Protocol` + URI pairs for identifying objects. These are currently used for: - * delivery destinations, eg ActivityPub inboxes, webmention targets, etc. - * copies of :class:`Object`s and :class:`User`s elsewhere, eg at:// URIs for - ATProto records, nevent etc bech32-encoded Nostr ids, ATProto user DIDs, - etc. - Used in StructuredPropertys inside :class:`Object` and :class:`User`; not - stored as top-level entities in the datastore. + * delivery destinations, eg ActivityPub inboxes, webmention targets, etc. + * copies of :class:`Object`\s and :class:`User`\s elsewhere, + eg ``at://`` URIs for ATProto records, nevent etc bech32-encoded Nostr ids, + ATProto user DIDs, etc. + + Used in :class:`google.cloud.ndb.model.StructuredProperty`\s inside + :class:`Object` and :class:`User`\; + not stored as top-level entities in the datastore. ndb implements this by hoisting each property here into a corresponding property on the parent entity, prefixed by the StructuredProperty name - below, eg `delivered.uri`, `delivered.protocol`, etc. + below, eg ``delivered.uri``, ``delivered.protocol``, etc. For repeated StructuredPropertys, the hoisted properties are all repeated on the parent entity, and reconstructed into StructuredPropertys based on their @@ -87,7 +89,7 @@ class Target(ndb.Model): class ProtocolUserMeta(type(ndb.Model)): - """:class:`User` metaclass. Registers all subclasses in the PROTOCOLS global.""" + """:class:`User` metaclass. Registers all subclasses in the ``PROTOCOLS`` global.""" def __new__(meta, name, bases, class_dict): cls = super().__new__(meta, name, bases, class_dict) @@ -100,7 +102,7 @@ class ProtocolUserMeta(type(ndb.Model)): def reset_protocol_properties(): - """Recreates various protocol properties to include choices PROTOCOLS.""" + """Recreates various protocol properties to include choices from ``PROTOCOLS``.""" Target.protocol = ndb.StringProperty( 'protocol', choices=list(PROTOCOLS.keys()), required=True) Object.source_protocol = ndb.StringProperty( @@ -119,11 +121,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): Stores some protocols' keypairs. Currently: * RSA keypair for ActivityPub HTTP Signatures - properties: mod, public_exponent, private_exponent, all encoded as - base64url (ie URL-safe base64) strings as described in RFC 4648 and - section 5.1 of the Magic Signatures spec + properties: ``mod``, ``public_exponent``, ``private_exponent``, all + encoded as base64url (ie URL-safe base64) strings as described in RFC + 4648 and section 5.1 of the Magic Signatures spec: https://tools.ietf.org/html/draft-cavage-http-signatures-12 - * *Not* K-256 signing or rotation keys for AT Protocol, those are stored in :class:`arroba.datastore_storage.AtpRepo` entities """ @@ -156,8 +157,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): def __init__(self, **kwargs): """Constructor. - Sets :attr:`obj` explicitly because however :class:`Model` sets it - doesn't work with @property and @obj.setter below. + Sets :attr:`obj` explicitly because however + :class:`google.cloud.ndb.model.Model` sets it doesn't work with + ``@property`` and ``@obj.setter`` below. """ obj = kwargs.pop('obj', None) super().__init__(**kwargs) @@ -175,7 +177,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): @classmethod def get_by_id(cls, id): - """Override Model.get_by_id to follow the use_instead property.""" + """Override :meth:`google.cloud.ndb.model.Model.get_by_id` to follow the + ``use_instead`` property. + """ user = cls._get_by_id(id) if user and user.use_instead: logger.info(f'{user.key} use_instead => {user.use_instead}') @@ -187,7 +191,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): def get_for_copy(copy_id): """Fetches a user with a given id in copies. - Thin wrapper around :meth:User.get_copies` that returns the first + Thin wrapper around :meth:`User.get_copies` that returns the first matching :class:`User`. """ users = User.get_for_copies([copy_id]) @@ -215,7 +219,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): @classmethod @ndb.transactional() def get_or_create(cls, id, propagate=False, **kwargs): - """Loads and returns a User. Creates it if necessary. + """Loads and returns a :class:`User`\. Creates it if necessary. Args: propagate (bool): whether to create copies of this user in push-based @@ -280,7 +284,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): """Loads :attr:`obj` for multiple users in parallel. Args: - users: sequence of :class:`User` + users (sequence of User) """ objs = ndb.get_multi(u.obj_key for u in users if u.obj_key) keys_to_objs = {o.key: o for o in objs if o} @@ -340,13 +344,19 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): return self.handle or self.key.id() def public_pem(self): - """Returns: bytes""" + """ + Returns: + bytes: + """ rsa = RSA.construct((base64_to_long(str(self.mod)), base64_to_long(str(self.public_exponent)))) return rsa.exportKey(format='PEM') def private_pem(self): - """Returns: bytes""" + """ + Returns: + bytes: + """ assert self.mod and self.public_exponent and self.private_exponent, str(self) rsa = RSA.construct((base64_to_long(str(self.mod)), base64_to_long(str(self.public_exponent)), @@ -354,7 +364,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): return rsa.exportKey(format='PEM') def name(self): - """Returns this user's human-readable name, eg 'Ryan Barrett'.""" + """Returns this user's human-readable name, eg ``Ryan Barrett``.""" if self.obj and self.obj.as1: name = self.obj.as1.get('displayName') if name: @@ -363,7 +373,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): return self.handle_or_id() def web_url(self): - """Returns this user's web URL (homepage), eg 'https://foo.com/'. + """Returns this user's web URL (homepage), eg ``https://foo.com/``. To be implemented by subclasses. @@ -376,10 +386,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): """Returns True if the given URL is this user's web URL (homepage). Args: - url: str + url (str) Returns: - boolean + bool: """ if not url: return False @@ -399,7 +409,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): """Returns this user's ActivityPub address, eg ``@me@foo.com``. Returns: - str + str: """ # TODO: use self.handle_as? need it to fall back to id? return f'@{self.handle_or_id()}@{self.ABBREV}{common.SUPERDOMAIN}' @@ -407,7 +417,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): def ap_actor(self, rest=None): """Returns this user's ActivityPub/AS2 actor id. - Eg ``https://atproto.brid.gy/ap/foo.com`. + Eg ``https://atproto.brid.gy/ap/foo.com``. May be overridden by subclasses. @@ -435,7 +445,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta): Defaults to this user's key id. Returns: - str + str: """ return self.key.id() @@ -516,11 +526,10 @@ class Object(StringIdModel): new = None changed = None - """ - Protocol and subclasses set these in fetch if this Object is new or if its - contents have changed from what was originally loaded from the datastore. - If either one is None, that means we don't know whether this Object is - new/changed. + """Protocol and subclasses set these in fetch if this :class:`Object` is + new or if its contents have changed from what was originally loaded from the + datastore. If either one is None, that means we don't know whether this + :class:`Object` is new/changed. :attr:`changed` is populated by :meth:`Object.activity_changed()`. """ @@ -664,12 +673,12 @@ class Object(StringIdModel): @classmethod def get_by_id(cls, id): - """Override Model.get_by_id to un-escape ^^ to #. + """Override :meth:`google.cloud.ndb.model.Model.get_by_id` to un-escape + ``^^`` to ``#``. Only needed for compatibility with historical URL paths, we're now back - to URL-encoding #s instead. + to URL-encoding ``#``s instead. https://github.com/snarfed/bridgy-fed/issues/469 - See "meth:`proxy_url()` for the inverse. """ return super().get_by_id(id.replace('^^', '#')) @@ -677,14 +686,14 @@ class Object(StringIdModel): @classmethod @ndb.transactional() def get_or_create(cls, id, **props): - """Returns an Object with the given property values. + """Returns an :class:`Object` with the given property values. - If a matching Object doesn't exist in the datastore, creates it first. - Only populates non-False/empty property values in props into the object. - Also populates the :attr:`new` and :attr:`changed` properties. + If a matching :class:`Object` doesn't exist in the datastore, creates it + first. Only populates non-False/empty property values in props into the + object. Also populates the :attr:`new` and :attr:`changed` properties. Returns: - :class:`Object` + Object: """ obj = cls.get_by_id(id) if obj: @@ -723,8 +732,8 @@ class Object(StringIdModel): Args: fetch_blobs (bool): whether to fetch images and other blobs, store - them in :class:`arroba.AtpRemoteBlob'\s if they don't already exist, - and fill them into the returned object. + them in :class:`arroba.datastore_storage.AtpRemoteBlob`\s if they + don't already exist, and fill them into the returned object. """ if self.bsky: return self.bsky @@ -744,14 +753,14 @@ class Object(StringIdModel): return {} def activity_changed(self, other_as1): - """Returns True if this activity is meaningfully changed from other_as1. + """Returns True if this activity is meaningfully changed from ``other_as1``. ...otherwise False. Used to populate :attr:`changed`. Args: - other_as1: dict AS1 object, or none + other_as1 (dict): AS1 object, or none """ return (as1.activity_changed(self.as1, other_as1) if self.as1 and other_as1 @@ -760,10 +769,11 @@ class Object(StringIdModel): def proxy_url(self): """Returns the Bridgy Fed proxy URL to render this post as HTML. - Note that some webmention receivers are struggling with the %23s - (URL-encoded #s) in these paths: - https://github.com/snarfed/bridgy-fed/issues/469 - https://github.com/pfefferle/wordpress-webmention/issues/359 + Note that some webmention receivers are struggling with the ``%23``s + (URL-encoded ``#``s) in these paths: + + * https://github.com/snarfed/bridgy-fed/issues/469 + * https://github.com/pfefferle/wordpress-webmention/issues/359 See "meth:`get_by_id()` for the inverse. """ @@ -845,16 +855,17 @@ class Follower(ndb.Model): @classmethod @ndb.transactional() def get_or_create(cls, *, from_, to, **kwargs): - """Returns a Follower with the given from_ and to users. + """Returns a Follower with the given ``from_`` and ``to`` users. - If a matching Follower doesn't exist in the datastore, creates it first. + If a matching :class:`Follower` doesn't exist in the datastore, creates + it first. Args: - from_: :class:`User` - to: :class:`User` + from_ (User) + to (User) Returns: - :class:`Follower` + Follower: """ assert from_ assert to @@ -878,11 +889,11 @@ class Follower(ndb.Model): def fetch_page(collection): """Fetches a page of Followers for the current user. - Wraps :func:`fetch_page`. Paging uses the `before` and `after` query + Wraps :func:`fetch_page`. Paging uses the ``before`` and ``after`` query parameters, if available in the request. Args: - collection, str, 'followers' or 'following' + collection (str): ``followers`` or ``following`` Returns: (followers, new_before, new_after) tuple with: @@ -913,22 +924,22 @@ class Follower(ndb.Model): def fetch_page(query, model_class): """Fetches a page of results from a datastore query. - Uses the `before` and `after` query params (if provided; should be ISO8601 - timestamps) and the queried model class's `updated` property to identify the - page to fetch. + Uses the ``before`` and ``after`` query params (if provided; should be + ISO8601 timestamps) and the queried model class's ``updated`` property to + identify the page to fetch. - Populates a `log_url_path` property on each result entity that points to a + Populates a ``log_url_path`` property on each result entity that points to a its most recent logged request. Args: - query: :class:`ndb.Query` - model_class: ndb model class + query (ndb.Query) + model_class (class) Returns: - (results, new_before, new_after) tuple with: - results: list of query result entities - new_before, new_after: str query param values for `before` and `after` - to fetch the previous and next pages, respectively + (list of entities, str, str) tuple: + (results, new_before, new_after), where new_before and new_after are query + param values for ``before`` and ``after`` to fetch the previous and next + pages, respectively """ # if there's a paging param ('before' or 'after'), update query with it # TODO: unify this with Bridgy's user page diff --git a/pages.py b/pages.py index d702810..11e430f 100644 --- a/pages.py +++ b/pages.py @@ -35,8 +35,8 @@ def load_user(protocol, id): """Loads the current request's user into `g.user`. Args: - protocol: str - id: str + protocol (str): + id (str): Raises: :class:`werkzeug.exceptions.HTTPException` on error or redirect @@ -231,19 +231,18 @@ def bridge_user(): def fetch_objects(query): - """Fetches a page of Object entities from a datastore query. + """Fetches a page of :class:`models.Object` entities from a datastore query. - Wraps :func:`models.fetch_page` and adds attributes to the returned Object - entities for rendering in objects.html. + Wraps :func:`models.fetch_page` and adds attributes to the returned + :class:`models.Object` entities for rendering in ``objects.html``. Args: - query: :class:`ndb.Query` + query (ndb.Query) Returns: - (results, new_before, new_after) tuple with: - results: list of Object entities - new_before, new_after: str query param values for `before` and `after` - to fetch the previous and next pages, respectively + (list of models.Object, str, str) tuple: + (results, new ``before`` query param, new ``after`` query param) + to fetch the previous and next pages, respectively """ objects, new_before, new_after = fetch_page(query, Object) diff --git a/protocol.py b/protocol.py index 47ca08a..fca30aa 100644 --- a/protocol.py +++ b/protocol.py @@ -52,9 +52,9 @@ class Protocol: """Base protocol class. Not to be instantiated; classmethods only. Attributes: - LABEL: str, human-readable lower case name - OTHER_LABELS: sequence of str, label aliases - ABBREV: str, lower case abbreviation, used in URL paths + LABEL (str): human-readable lower case name + OTHER_LABELS (sequence): of str, label aliases + ABBREV (str): lower case abbreviation, used in URL paths """ ABBREV = None OTHER_LABELS = () @@ -74,13 +74,13 @@ class Protocol: ...based on the request's hostname. Args: - fed (str or Protocol): protocol to return if the current request is on - ``fed.brid.gy`` + fed (str or protocol.Protocol): protocol to return if the current + request is on ``fed.brid.gy`` Returns: - Protocol subclass: ...or None if the provided domain or request - hostname domain is not a subdomain of ``brid.gy` or isn't a known - protocol + protocol.Protocol subclass: protocol, or None if the provided domain + or request hostname domain is not a subdomain of ``brid.gy` or isn't + a known protocol """ return Protocol.for_bridgy_subdomain(request.host, fed=fed) @@ -89,14 +89,13 @@ class Protocol: """Returns the protocol for a brid.gy subdomain. Args: - domain_or_url: str - fed (str or Protocol): protocol to return if the current request is on - ``fed.brid.gy`` + domain_or_url (str) + fed (str or protocol.Protocol): protocol to return if the current + request is on ``fed.brid.gy`` - Returns: - Protocol subclass: ...or None if the provided domain or request - hostname domain is not a subdomain of ``brid.gy` or isn't a known - protocol + Returns: protocol.Protocol subclass: protocol, or None if the provided + domain or request hostname domain is not a subdomain of ``brid.gy` or + isn't a known protocol """ domain = (util.domain_from_link(domain_or_url, minimize=False) if util.is_web(domain_or_url) @@ -116,10 +115,10 @@ class Protocol: 'https://ap.brid.gy/foo/bar'. Args: - path: str + path (str) Returns: - str, URL + str: URL """ return urljoin(f'https://{cls.ABBREV or "fed"}{common.SUPERDOMAIN}/', path) @@ -196,7 +195,7 @@ class Protocol: @classmethod def key_for(cls, id): - """Returns the :class:`ndb.Key` for a given id's :class:`User`. + """Returns the :class:`ndb.Key` for a given id's :class:`models.User`. To be implemented by subclasses. Canonicalizes the id if necessary. @@ -344,8 +343,8 @@ class Protocol: default_g_user is True, otherwise None. Args: - obj: :class:`Object` - default_g_user: boolean + obj (models.Object) + default_g_user (bool) Returns: :class:`ndb.Key` or None @@ -363,9 +362,9 @@ class Protocol: To be implemented by subclasses. Args: - obj: :class:`Object` with activity to send - url: str, destination URL to send to - log_data: boolean, whether to log full data object + obj (models.Object): with activity to send + url (str): destination URL to send to + log_data (bool): whether to log full data object Returns: True if the activity is sent successfully, False if it is ignored or @@ -389,7 +388,7 @@ class Protocol: To be implemented by subclasses. Args: - obj: :class:`Object` with the id to fetch. Data is filled into one of + obj (models.Object): with the id to fetch. Data is filled into one of the protocol-specific properties, eg as2, mf2, bsky. **kwargs: subclass-specific @@ -413,7 +412,7 @@ class Protocol: To be implemented by subclasses. Args: - obj: :class:`Object` + obj (models.Object): Returns: (response body, dict with HTTP headers) tuple appropriate to be @@ -436,8 +435,8 @@ class Protocol: inbox. Args: - obj: :class:`Object` - shared: boolean, optional. If `True`, returns a common/shared + obj (models.Object): + shared (bool): optional. If `True`, returns a common/shared endpoint, eg ActivityPub's `sharedInbox`, that can be reused for multiple recipients for efficiency @@ -453,9 +452,9 @@ class Protocol: Default implementation here, subclasses may override. Args: - url: str + url (str): - Returns: boolean + Returns: bool """ return util.domain_or_parent_in(util.domain_from_link(url), DOMAIN_BLOCKLIST + DOMAINS) @@ -468,7 +467,7 @@ class Protocol: raises :class:`werkzeug.exceptions.BadRequest`. Args: - obj: :class:`Object` + obj (models.Object): Returns: (response body, HTTP status code) tuple for Flask response @@ -639,7 +638,7 @@ class Protocol: """Handles an incoming follow activity. Args: - obj: :class:`Object`, follow activity + obj (models.Object): follow activity """ logger.info('Got follow. Loading users, storing Follow(s), sending accept(s)') @@ -732,11 +731,10 @@ class Protocol: Checks if we've seen it before. Args: - obj: :class:`Object` + obj (models.Object) Returns: - obj: :class:`Object`, the same one if the input obj is an activity, - otherwise a new one + Object: ``obj`` if it's an activity, otherwise a new object """ if obj.type not in ('note', 'article', 'comment'): return obj @@ -795,7 +793,7 @@ class Protocol: """Delivers an activity to its external recipients. Args: - obj: :class:`Object`, activity to deliver + obj (models.Object): activity to deliver """ # find delivery targets # sort targets so order is deterministic for tests, debugging, etc @@ -865,13 +863,11 @@ class Protocol: Targets are both objects - original posts, events, etc - and actors. Args: - obj (:class:`models.Object`) + obj (models.Object) Returns: - dict: { - :class:`Target`: original (in response to) :class:`models.Object`, - if any, otherwise None - } + dict: maps :class:`Target`: to original (in response to) + :class:`models.Object`, if any, otherwise None """ logger.info('Finding recipients and their targets') @@ -1000,24 +996,25 @@ class Protocol: Note that :meth:`Object._post_put_hook` updates the cache. Args: - id: str - - remote: boolean, whether to fetch the object over the network. If True, + id (str) + remote (bool): whether to fetch the object over the network. If True, fetches even if we already have the object stored, and updates our stored copy. If False and we don't have the object stored, returns None. Default (None) means to fetch over the network only if we don't already have it stored. - local: boolean, whether to load from the datastore before + local (bool): whether to load from the datastore before fetching over the network. If False, still stores back to the datastore after a successful remote fetch. kwargs: passed through to :meth:`fetch()` - Returns: :class:`Object`, or None if: + Returns + models.Object: loaded object, or None if: + * it isn't fetchable, eg a non-URL string for Web - * remote is False and it isn't in the cache or datastore + * ``remote`` is False and it isn't in the cache or datastore Raises: - :class:`requests.HTTPError`, anything else that :meth:`fetch` raises + requests.HTTPError: anything that :meth:`fetch` raises """ assert local or remote is not False @@ -1081,19 +1078,19 @@ class Protocol: @app.post('/queue/receive') def receive_task(): - """Task handler for a newly received :class:`Object`. + """Task handler for a newly received :class:`models.Object`. - Form parameters: + Parameters: + * obj (ndb.Key): :class:`models.Object` to handle + * user (ndb.Key): :class:`models.User` this activity is on behalf of. This + user will be loaded into ``g.user`` - * obj: urlsafe :class:`ndb.Key` of the :class:`Object` to handle - * user: urlsafe :class:`ndb.Key` of the :class:`User` this activity is on - behalf of. This user will be loaded into `g.user`. - - TODO: migrate incoming webmentions and AP inbox deliveries to this. - difficulty is that parts of Protocol.receive depend on setup in - Web.webmention and ActivityPub.inbox, eg Object with new/changed, g.user - (which receive now loads), HTTP request details, etc. see stash for attempt - at this for Web. + TODO: migrate incoming webmentions and AP inbox deliveries to this. The + difficulty is that parts of :meth:`protocol.Protocol.receive` depend on + setup in :func:`web.webmention` and :func:`activitypub.inbox`, eg + :class:`models.Object` with ``new`` and ``changed``, ``g.user`` (which + :meth:`receive` now loads), HTTP request details, etc. See stash for attempt + at this for :class:`web.Web`. """ logger.info(f'Params: {list(request.form.items())}') diff --git a/redirect.py b/redirect.py index e200a17..b256a34 100644 --- a/redirect.py +++ b/redirect.py @@ -1,18 +1,18 @@ """Simple conneg endpoint that serves AS2 or redirects to to the original post. -Only for Web users. Other protocols (including Web sometimes) use /convert/ in -convert.py instead. +Only for :class:`web.Web` users. Other protocols (including :class:`web.Web` + sometimes) use ``/`` convert ``/`` in convert.py instead. -Serves /r/https://foo.com/bar URL paths, where https://foo.com/bar is a original -post for a Web user. Needed for Mastodon interop, they require that AS2 object -ids and urls are on the same domain that serves them. Background: +Serves ``/r/https://foo.com/bar`` URL paths, where ``https://foo.com/bar`` is a +original post for a :class:`Web` user. Needed for Mastodon interop, they require +that AS2 object ids and urls are on the same domain that serves them. +Background: -https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599 -https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747 +* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599 +* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747 -The conneg makes these /r/ URLs searchable in Mastodon: +The conneg makes these ``/r/`` URLs searchable in Mastodon: https://github.com/snarfed/bridgy-fed/issues/352 - """ import logging import re @@ -47,9 +47,9 @@ DOMAIN_ALLOWLIST = frozenset(( def redir(to): """Either redirect to a given URL or convert it to another format. - E.g. redirects /r/https://foo.com/bar?baz to https://foo.com/bar?baz, or if - it's requested with AS2 conneg in the Accept header, fetches and converts - and serves it as AS2. + E.g. redirects ``/r/https://foo.com/bar?baz`` to + ``https://foo.com/bar?baz``, or if it's requested with AS2 conneg in the + ``Accept`` header, fetches and converts and serves it as AS2. """ if request.args: to += '?' + urllib.parse.urlencode(request.args) diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index fc4c058..bebc645 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -1,4 +1,3 @@ -# coding=utf-8 """Unit tests for activitypub.py.""" from base64 import b64encode import copy diff --git a/tests/test_models.py b/tests/test_models.py index d523ceb..de8d57f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,3 @@ -# coding=utf-8 """Unit tests for models.py.""" from unittest.mock import patch diff --git a/tests/test_web.py b/tests/test_web.py index f1f2fd7..f2c0e94 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,4 +1,3 @@ -# coding=utf-8 """Unit tests for webmention.py.""" import copy from unittest.mock import patch diff --git a/web.py b/web.py index 611a692..381905a 100644 --- a/web.py +++ b/web.py @@ -1,4 +1,4 @@ -"""Handles inbound webmentions.""" +"""Webmention protocol with microformats2 in HTML, aka the IndieWeb stack.""" import datetime import difflib import logging @@ -109,7 +109,7 @@ class Web(User, Protocol): profile_id = web_url def ap_address(self): - """Returns this user's ActivityPub address, eg '@foo.com@foo.com'. + """Returns this user's ActivityPub address, eg ``@foo.com@foo.com``. Uses the user's domain if they're direct, fed.brid.gy if they're not. """ @@ -147,7 +147,8 @@ class Web(User, Protocol): Uses stored representative h-card if available, falls back to id. - Returns: str + Returns: + str: """ id = self.key.id() @@ -171,9 +172,9 @@ class Web(User, Protocol): """Fetches site a couple ways to check for redirects and h-card. - Returns: :class:`Web` that was verified. May be different than - self! eg if self's domain started with www and we switch to the root - domain. + Returns: + web.Web: user that was verified. May be different than self! eg if + self 's domain started with www and we switch to the root domain. """ domain = self.key.id() logger.info(f'Verifying {domain}') @@ -333,16 +334,17 @@ class Web(User, Protocol): def fetch(cls, obj, gateway=False, check_backlink=False, **kwargs): """Fetches a URL over HTTP and extracts its microformats2. - Follows redirects, but doesn't change the original URL in obj's id! The - :class:`Model` class doesn't allow that anyway, but more importantly, we - want to preserve that original URL becase other objects may refer to it - instead of the final redirect destination URL. + Follows redirects, but doesn't change the original URL in ``obj``'s id! + The :class:`Model` class doesn't allow that anyway, but more + importantly, we want to preserve that original URL becase other objects + may refer to it instead of the final redirect destination URL. See :meth:`Protocol.fetch` for other background. Args: - gateway: passed through to :func:`webutil.util.fetch_mf2` - check_backlink: bool, optional, whether to require a link to Bridgy + gateway (bool): passed through to + :func:`oauth_dropins.webutil.util.fetch_mf2` + check_backlink (bool): optional, whether to require a link to Bridgy Fed. Ignored if the URL is a homepage, ie has no path. kwargs: ignored """ diff --git a/webfinger.py b/webfinger.py index b30e962..304f576 100644 --- a/webfinger.py +++ b/webfinger.py @@ -157,7 +157,7 @@ class Webfinger(flask_util.XrdOrJrd): class HostMeta(flask_util.XrdOrJrd): - """Renders and serves the /.well-known/host-meta file. + """Renders and serves the ``/.well-known/host-meta`` file. Supports both JRD and XRD; defaults to XRD. https://tools.ietf.org/html/rfc6415#section-3 @@ -173,7 +173,7 @@ class HostMeta(flask_util.XrdOrJrd): @app.get('/.well-known/host-meta.xrds') def host_meta_xrds(): - """Renders and serves the /.well-known/host-meta.xrds XRDS-Simple file.""" + """Renders and serves the ``/.well-known/host-meta.xrds`` XRDS-Simple file.""" return (render_template('host-meta.xrds', host_uri=common.host_url()), {'Content-Type': 'application/xrds+xml'}) @@ -187,8 +187,8 @@ def fetch(addr): returning None Args: - addr (str): a Webfinger-compatible address, eg @x@y, acct:x@y, or - https://x/y + addr (str): a Webfinger-compatible address, eg ``@x@y``, ``acct:x@y``, or + ``https://x/y`` Returns: dict: fetched WebFinger data, or None on error