kopia lustrzana https://gitlab.com/jaywink/federation
Fix some tests and fix code that was failing tests. Catch HTML signatures with invalid padding.
rodzic
1f8d4ac93f
commit
24f5bb21a9
|
@ -75,8 +75,8 @@ def verify_ld_signature(payload):
|
|||
obj_digest = hash(obj)
|
||||
digest = (sig_digest + obj_digest).encode('utf-8')
|
||||
|
||||
sig_value = b64decode(signature.get('signatureValue'))
|
||||
try:
|
||||
sig_value = b64decode(signature.get('signatureValue'))
|
||||
verifier.verify(SHA256.new(digest), sig_value)
|
||||
logger.debug('ld_signature - %s has a valid signature', payload.get("id"))
|
||||
return profile.id
|
||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from operator import attrgetter
|
||||
from typing import List, Dict, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
@ -801,8 +801,9 @@ class Note(Object, RawContentMixin):
|
|||
for el in self._soup('a', attrs={'class':'hashtag'}):
|
||||
self.tag_objects.append(Hashtag(
|
||||
href = el.attrs['href'],
|
||||
name = el.text.lstrip('#')
|
||||
name = el.text
|
||||
))
|
||||
self.tag_objects = sorted(self.tag_objects, key=attrgetter('name'))
|
||||
if el.text == '#nsfw': self.sensitive = True
|
||||
|
||||
# Add Mention objects
|
||||
|
|
|
@ -237,8 +237,8 @@ class RawContentMixin(BaseEntity):
|
|||
@property
|
||||
def tags(self) -> List[str]:
|
||||
if not self.raw_content:
|
||||
return
|
||||
return find_tags(self.raw_content)
|
||||
return []
|
||||
return sorted(find_tags(self.raw_content))
|
||||
|
||||
def extract_mentions(self):
|
||||
if not self.raw_content:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import commonmark
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from pprint import pprint
|
||||
|
@ -9,6 +10,7 @@ from federation.entities.activitypub.models import context_manager
|
|||
from federation.entities.activitypub.models import Accept
|
||||
from federation.tests.fixtures.keys import PUBKEY
|
||||
from federation.types import UserType
|
||||
from federation.utils.text import process_text_links
|
||||
|
||||
|
||||
class TestEntitiesConvertToAS2:
|
||||
|
@ -65,6 +67,8 @@ class TestEntitiesConvertToAS2:
|
|||
|
||||
def test_comment_to_as2__url_in_raw_content(self, activitypubcomment):
|
||||
activitypubcomment.raw_content = 'raw_content http://example.com'
|
||||
activitypubcomment.rendered_content = process_text_links(
|
||||
commonmark.commonmark(activitypubcomment.raw_content).strip())
|
||||
activitypubcomment.pre_send()
|
||||
result = activitypubcomment.to_as2()
|
||||
assert result == {
|
||||
|
@ -118,6 +122,7 @@ class TestEntitiesConvertToAS2:
|
|||
}
|
||||
|
||||
def test_post_to_as2(self, activitypubpost):
|
||||
activitypubpost.rendered_content = commonmark.commonmark(activitypubpost.raw_content).strip()
|
||||
activitypubpost.pre_send()
|
||||
result = activitypubpost.to_as2()
|
||||
assert result == {
|
||||
|
@ -191,6 +196,15 @@ class TestEntitiesConvertToAS2:
|
|||
}
|
||||
|
||||
def test_post_to_as2__with_tags(self, activitypubpost_tags):
|
||||
activitypubpost_tags.rendered_content = '<h1>raw_content</h1>\n' \
|
||||
'<p><a class="hashtag" ' \
|
||||
'href="https://example.com/tag/foobar/" rel="noopener ' \
|
||||
'noreferrer nofollow" ' \
|
||||
'target="_blank">#<span>foobar</span></a>\n' \
|
||||
'<a class="hashtag" ' \
|
||||
'href="https://example.com/tag/barfoo/" rel="noopener ' \
|
||||
'noreferrer nofollow" ' \
|
||||
'target="_blank">#<span>barfoo</span></a></p>'
|
||||
activitypubpost_tags.pre_send()
|
||||
result = activitypubpost_tags.to_as2()
|
||||
assert result == {
|
||||
|
@ -204,11 +218,11 @@ class TestEntitiesConvertToAS2:
|
|||
'url': 'http://127.0.0.1:8000/post/123456/',
|
||||
'attributedTo': 'http://127.0.0.1:8000/profile/123456/',
|
||||
'content': '<h1>raw_content</h1>\n'
|
||||
'<p><a class="mention hashtag" '
|
||||
'<p><a class="hashtag" '
|
||||
'href="https://example.com/tag/foobar/" rel="noopener '
|
||||
'noreferrer nofollow" '
|
||||
'target="_blank">#<span>foobar</span></a>\n'
|
||||
'<a class="mention hashtag" '
|
||||
'<a class="hashtag" '
|
||||
'href="https://example.com/tag/barfoo/" rel="noopener '
|
||||
'noreferrer nofollow" '
|
||||
'target="_blank">#<span>barfoo</span></a></p>',
|
||||
|
@ -235,6 +249,7 @@ class TestEntitiesConvertToAS2:
|
|||
}
|
||||
|
||||
def test_post_to_as2__with_images(self, activitypubpost_images):
|
||||
activitypubpost_images.rendered_content = '<p>raw_content</p>'
|
||||
activitypubpost_images.pre_send()
|
||||
result = activitypubpost_images.to_as2()
|
||||
assert result == {
|
||||
|
@ -274,6 +289,7 @@ class TestEntitiesConvertToAS2:
|
|||
}
|
||||
|
||||
def test_post_to_as2__with_diaspora_guid(self, activitypubpost_diaspora_guid):
|
||||
activitypubpost_diaspora_guid.rendered_content = '<p>raw_content</p>'
|
||||
activitypubpost_diaspora_guid.pre_send()
|
||||
result = activitypubpost_diaspora_guid.to_as2()
|
||||
assert result == {
|
||||
|
@ -418,17 +434,6 @@ class TestEntitiesPostReceive:
|
|||
"public": False,
|
||||
}]
|
||||
|
||||
@patch("federation.entities.activitypub.models.bleach.linkify", autospec=True)
|
||||
def test_post_post_receive__linkifies_if_not_markdown(self, mock_linkify, activitypubpost):
|
||||
activitypubpost._media_type = 'text/html'
|
||||
activitypubpost.post_receive()
|
||||
mock_linkify.assert_called_once()
|
||||
|
||||
@patch("federation.entities.activitypub.models.bleach.linkify", autospec=True)
|
||||
def test_post_post_receive__skips_linkify_if_markdown(self, mock_linkify, activitypubpost):
|
||||
activitypubpost.post_receive()
|
||||
mock_linkify.assert_not_called()
|
||||
|
||||
|
||||
class TestEntitiesPreSend:
|
||||
def test_post_inline_images_are_attached(self, activitypubpost_embedded_images):
|
||||
|
|
|
@ -4,6 +4,9 @@ from unittest.mock import patch, Mock, DEFAULT
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from federation.entities.activitypub.models import Person
|
||||
|
||||
|
||||
#from federation.entities.activitypub.entities import (
|
||||
# models.Follow, models.Accept, models.Person, models.Note, models.Note,
|
||||
# models.Delete, models.Announce)
|
||||
|
@ -70,9 +73,7 @@ class TestActivitypubEntityMappersReceive:
|
|||
post = entities[0]
|
||||
assert isinstance(post, models.Note)
|
||||
assert isinstance(post, Post)
|
||||
assert post.raw_content == '<p><span class="h-card"><a class="u-url mention" ' \
|
||||
'href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
'@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.raw_content == ''
|
||||
assert post.rendered_content == '<p><span class="h-card"><a class="u-url mention" href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
'@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237"
|
||||
|
@ -87,40 +88,41 @@ class TestActivitypubEntityMappersReceive:
|
|||
post = entities[0]
|
||||
assert isinstance(post, models.Note)
|
||||
assert isinstance(post, Post)
|
||||
assert post.raw_content == '<p>boom #test</p>'
|
||||
assert post.raw_content == ''
|
||||
assert post.rendered_content == '<p>boom <a class="mention hashtag" data-hashtag="test" href="https://mastodon.social/tags/test" rel="tag">#<span>test</span></a></p>'
|
||||
|
||||
# TODO: fix this test
|
||||
@pytest.mark.skip
|
||||
def test_message_to_objects_simple_post__with_mentions(self):
|
||||
@patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev3.jasonrobinson.me"))
|
||||
def test_message_to_objects_simple_post__with_mentions(self, mock_get):
|
||||
entities = message_to_objects(ACTIVITYPUB_POST_WITH_MENTIONS, "https://mastodon.social/users/jaywink")
|
||||
assert len(entities) == 1
|
||||
post = entities[0]
|
||||
assert isinstance(post, models.Note)
|
||||
assert isinstance(post, Post)
|
||||
assert len(post._mentions) == 1
|
||||
assert list(post._mentions)[0] == "https://dev3.jasonrobinson.me/u/jaywink/"
|
||||
assert list(post._mentions)[0] == "jaywink@dev3.jasonrobinson.me"
|
||||
|
||||
def test_message_to_objects_simple_post__with_source__bbcode(self):
|
||||
|
||||
@patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me"))
|
||||
def test_message_to_objects_simple_post__with_source__bbcode(self, mock_get):
|
||||
entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, "https://diaspodon.fr/users/jaywink")
|
||||
assert len(entities) == 1
|
||||
post = entities[0]
|
||||
assert isinstance(post, models.Note)
|
||||
assert isinstance(post, Post)
|
||||
assert post.rendered_content == '<p><span class="h-card"><a class="u-url mention" href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
assert post.rendered_content == '<p><span class="h-card"><a class="u-url mention" data-mention="jaywink@dev.jasonrobinson.me" href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
'@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.raw_content == '<p><span class="h-card"><a class="u-url mention" ' \
|
||||
'href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
'@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.raw_content == ''
|
||||
|
||||
def test_message_to_objects_simple_post__with_source__markdown(self):
|
||||
@patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me"))
|
||||
def test_message_to_objects_simple_post__with_source__markdown(self, mock_get):
|
||||
entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, "https://diaspodon.fr/users/jaywink")
|
||||
assert len(entities) == 1
|
||||
post = entities[0]
|
||||
assert isinstance(post, models.Note)
|
||||
assert isinstance(post, Post)
|
||||
assert post.rendered_content == '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" ' \
|
||||
'class="u-url mention">@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.raw_content == "@jaywink boom"
|
||||
assert post.rendered_content == '<p><span class="h-card"><a class="u-url mention" ' \
|
||||
'href="https://dev.jasonrobinson.me/u/jaywink/">@<span>jaywink</span></a></span> boom</p>'
|
||||
assert post.raw_content == "@jaywink@dev.jasonrobinson.me boom"
|
||||
assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237"
|
||||
assert post.actor_id == "https://diaspodon.fr/users/jaywink"
|
||||
assert post.public is True
|
||||
|
@ -145,15 +147,17 @@ class TestActivitypubEntityMappersReceive:
|
|||
assert photo.guid == ""
|
||||
assert photo.handle == ""
|
||||
|
||||
def test_message_to_objects_comment(self):
|
||||
@patch("federation.entities.activitypub.models.get_profile_or_entity", return_value=Person(finger="jaywink@dev.jasonrobinson.me"))
|
||||
def test_message_to_objects_comment(self, mock_get):
|
||||
entities = message_to_objects(ACTIVITYPUB_COMMENT, "https://diaspodon.fr/users/jaywink")
|
||||
assert len(entities) == 1
|
||||
comment = entities[0]
|
||||
assert isinstance(comment, models.Note)
|
||||
assert isinstance(comment, Comment)
|
||||
assert comment.raw_content == '<p><span class="h-card"><a class="u-url mention" ' \
|
||||
assert comment.rendered_content == '<p><span class="h-card"><a class="u-url mention" data-mention="jaywink@dev.jasonrobinson.me" ' \
|
||||
'href="https://dev.jasonrobinson.me/u/jaywink/">' \
|
||||
'@<span>jaywink</span></a></span> boom</p>'
|
||||
assert comment.raw_content == ''
|
||||
assert comment.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237"
|
||||
assert comment.actor_id == "https://diaspodon.fr/users/jaywink"
|
||||
assert comment.target_id == "https://dev.jasonrobinson.me/content/653bad70-41b3-42c9-89cb-c4ee587e68e4/"
|
||||
|
|
|
@ -30,6 +30,7 @@ def activitypubcomment():
|
|||
with freeze_time("2019-04-27"):
|
||||
obj = models.Comment(
|
||||
raw_content="raw_content",
|
||||
rendered_content="<p>raw_content</p>",
|
||||
public=True,
|
||||
provider_display_name="Socialhome",
|
||||
id=f"http://127.0.0.1:8000/post/123456/",
|
||||
|
|
|
@ -35,7 +35,7 @@ ACTIVITYPUB_COMMENT = {
|
|||
'contentMap': {'en': '<p><span class="h-card"><a class="u-url mention" href="https://dev.jasonrobinson.me/u/jaywink/">@<span>jaywink</span></a></span> boom</p>'},
|
||||
'attachment': [],
|
||||
'tag': [{'type': 'Mention',
|
||||
'href': 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/',
|
||||
'href': 'https://dev.jasonrobinson.me/u/jaywink/',
|
||||
'name': '@jaywink@dev.jasonrobinson.me'}],
|
||||
'replies': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
|
||||
'type': 'Collection',
|
||||
|
@ -459,9 +459,9 @@ ACTIVITYPUB_POST_WITH_TAGS = {
|
|||
'conversation': 'tag:diaspodon.fr,2019-06-28:objectId=2347687:objectType=Conversation',
|
||||
'content': '<p>boom <a href="https://mastodon.social/tags/test" class="mention hashtag" rel="tag">#<span>test</span></a></p>',
|
||||
'attachment': [],
|
||||
'tag': [{'type': 'Mention',
|
||||
'href': 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/',
|
||||
'name': '@jaywink@dev.jasonrobinson.me'}],
|
||||
'tag': [{'type': 'Hashtag',
|
||||
'href': 'https://mastodon.social/tags/test',
|
||||
'name': '#test'}],
|
||||
'replies': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
|
||||
'type': 'Collection',
|
||||
'first': {'type': 'CollectionPage',
|
||||
|
@ -552,13 +552,13 @@ ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN = {
|
|||
'conversation': 'tag:diaspodon.fr,2019-06-28:objectId=2347687:objectType=Conversation',
|
||||
'content': '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" class="u-url mention">@<span>jaywink</span></a></span> boom</p>',
|
||||
'source': {
|
||||
'content': "@jaywink boom",
|
||||
'content': "@{jaywink@dev.jasonrobinson.me} boom",
|
||||
'mediaType': "text/markdown",
|
||||
},
|
||||
'contentMap': {'en': '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" class="u-url mention">@<span>jaywink</span></a></span> boom</p>'},
|
||||
'attachment': [],
|
||||
'tag': [{'type': 'Mention',
|
||||
'href': 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/',
|
||||
'href': 'https://dev.jasonrobinson.me/u/jaywink/',
|
||||
'name': '@jaywink@dev.jasonrobinson.me'}],
|
||||
'replies': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
|
||||
'type': 'Collection',
|
||||
|
@ -612,7 +612,7 @@ ACTIVITYPUB_POST_WITH_SOURCE_BBCODE = {
|
|||
'contentMap': {'en': '<p><span class="h-card"><a class="u-url mention" href="https://dev.jasonrobinson.me/u/jaywink/">@<span>jaywink</span></a></span> boom</p>'},
|
||||
'attachment': [],
|
||||
'tag': [{'type': 'Mention',
|
||||
'href': 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/',
|
||||
'href': 'https://dev.jasonrobinson.me/u/jaywink/',
|
||||
'name': '@jaywink@dev.jasonrobinson.me'}],
|
||||
'replies': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
|
||||
'type': 'Collection',
|
||||
|
|
|
@ -18,78 +18,49 @@ class TestFindTags:
|
|||
|
||||
def test_all_tags_are_parsed_from_text(self):
|
||||
source = "#starting and #MixED with some #line\nendings also tags can\n#start on new line"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"starting", "mixed", "line", "start"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "#starting/starting and #MixED/mixed with some #line/line\nendings also tags can\n" \
|
||||
"#start/start on new line"
|
||||
|
||||
def test_code_block_tags_ignored(self):
|
||||
source = "foo\n```\n#code\n```\n#notcode\n\n #alsocode\n"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"notcode"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "foo\n```\n#code\n```\n#notcode/notcode\n\n #alsocode\n"
|
||||
|
||||
def test_endings_are_filtered_out(self):
|
||||
source = "#parenthesis) #exp! #list] *#doh* _#bah_ #gah% #foo/#bar"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"parenthesis", "exp", "list", "doh", "bah", "gah", "foo", "bar"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "#parenthesis/parenthesis) #exp/exp! #list/list] *#doh/doh* _#bah/bah_ #gah/gah% " \
|
||||
"#foo/foo/#bar/bar"
|
||||
|
||||
def test_finds_tags(self):
|
||||
source = "#post **Foobar** #tag #OtherTag #third\n#fourth"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"third", "fourth", "post", "othertag", "tag"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "#post/post **Foobar** #tag/tag #OtherTag/othertag #third/third\n#fourth/fourth"
|
||||
|
||||
def test_ok_with_html_tags_in_text(self):
|
||||
source = "<p>#starting and <span>#MixED</span> however not <#>this</#> or <#/>that"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"starting", "mixed"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "<p>#starting/starting and <span>#MixED/mixed</span> however not <#>this</#> or <#/>that"
|
||||
|
||||
def test_postfixed_tags(self):
|
||||
source = "#foo) #bar] #hoo, #hee."
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"foo", "bar", "hoo", "hee"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "#foo/foo) #bar/bar] #hoo/hoo, #hee/hee."
|
||||
|
||||
def test_prefixed_tags(self):
|
||||
source = "(#foo [#bar"
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"foo", "bar"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == "(#foo/foo [#bar/bar"
|
||||
|
||||
def test_invalid_text_returns_no_tags(self):
|
||||
source = "#a!a #a#a #a$a #a%a #a^a #a&a #a*a #a+a #a.a #a,a #a@a #a£a #a(a #a)a #a=a " \
|
||||
"#a?a #a`a #a'a #a\\a #a{a #a[a #a]a #a}a #a~a #a;a #a:a #a\"a #a’a #a”a #\xa0cd"
|
||||
tags, text = find_tags(source)
|
||||
assert tags == set()
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == source
|
||||
tags = find_tags(source)
|
||||
assert tags == {'a'}
|
||||
|
||||
def test_start_of_paragraph_in_html_content(self):
|
||||
source = '<p>First line</p><p>#foobar #barfoo</p>'
|
||||
tags, text = find_tags(source)
|
||||
tags = find_tags(source)
|
||||
assert tags == {"foobar", "barfoo"}
|
||||
assert text == source
|
||||
tags, text = find_tags(source, replacer=self._replacer)
|
||||
assert text == '<p>First line</p><p>#foobar/foobar #barfoo/barfoo</p>'
|
||||
|
||||
|
||||
class TestProcessTextLinks:
|
||||
|
|
|
@ -27,7 +27,7 @@ def encode_if_text(text):
|
|||
return text
|
||||
|
||||
|
||||
def find_tags(text: str) -> List[str]:
|
||||
def find_tags(text: str) -> Set[str]:
|
||||
"""Find tags in text.
|
||||
|
||||
Ignore tags inside code blocks.
|
||||
|
@ -37,7 +37,7 @@ def find_tags(text: str) -> List[str]:
|
|||
"""
|
||||
tags = find_elements(BeautifulSoup(commonmark(text, ignore_html_blocks=True), 'html.parser'),
|
||||
TAG_PATTERN)
|
||||
return sorted([tag.text.lstrip('#').lower() for tag in tags])
|
||||
return set([tag.text.lstrip('#').lower() for tag in tags])
|
||||
|
||||
|
||||
def find_elements(soup: BeautifulSoup, pattern: re.Pattern) -> List[NavigableString]:
|
||||
|
@ -54,7 +54,7 @@ def find_elements(soup: BeautifulSoup, pattern: re.Pattern) -> List[NavigableStr
|
|||
if candidate.parent.name == 'code': continue
|
||||
ns = [NavigableString(r) for r in re.split(pattern, candidate.text)]
|
||||
candidate.replace_with(*ns)
|
||||
return list(soup.find_all(string=re.compile(r'^'+pattern.pattern)))
|
||||
return list(soup.find_all(string=re.compile(r'\A'+pattern.pattern+r'\Z')))
|
||||
|
||||
|
||||
def get_path_from_url(url: str) -> str:
|
||||
|
|
Ładowanie…
Reference in New Issue