Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions manual_testing/mastodon_manual_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from abstractions import AdoptablePet
from social_posters.mastodon import PosterMastodon

def postExceed500CharsLimitWithAdoptionLink():
def post_exceed_500_chars_limit_with_adoption_link():
pet = AdoptablePet("Brian",
"Labrador Retriever",
"White Labrador",
"Quahog",
"I am a writer! Post exceeds limit with adoption link"*1000,
"I am a writer! Post exceeds limit with adoption link"*200,
"http://www.davidgorman.com/4quartets/",
"https://static.wikia.nocookie.net/familyguy/images/c/c2/FamilyGuy_Single_BrianWriter_R7.jpg/revision/latest?cb=20230807152447",
11,
Expand All @@ -20,7 +20,7 @@ def postExceed500CharsLimitWithAdoptionLink():
)
return pet

def postExceed500CharsLimitWithoutAdoptionLink():
def post_exceed_500_chars_limit_without_adoption_link():
pet = AdoptablePet("Vinny",
"Unknown",
"Unknown",
Expand All @@ -36,7 +36,7 @@ def postExceed500CharsLimitWithoutAdoptionLink():
return pet


def postWithin500CharsLimitWithAdoptionLink():
def post_within_500_chars_limit_with_adoption_link():
pet = AdoptablePet("Ernie",
"Chicken",
"Unknown",
Expand All @@ -51,7 +51,7 @@ def postWithin500CharsLimitWithAdoptionLink():
)
return pet

def postWithin500CharsLimitWithoutAdoptionLink():
def post_within_500_chars_limit_without_adoption_link():
pet = AdoptablePet("Pouncy",
"Cat",
"Unknown",
Expand All @@ -66,7 +66,7 @@ def postWithin500CharsLimitWithoutAdoptionLink():
)
return pet

def postUnicode():
def post_unicode():
pet = AdoptablePet("Vinny",
"Unknown",
"Unknown",
Expand All @@ -81,12 +81,14 @@ def postUnicode():
)
return pet

# !!!DO NOT TEST MULTIPLE CASES AT THE SAME TIME!!!
# !!!OTHERWISE YOU MAY TRIGGER SPAM DETECTION!!!
testingCases = [
postExceed500CharsLimitWithAdoptionLink,
postExceed500CharsLimitWithoutAdoptionLink,
postWithin500CharsLimitWithAdoptionLink,
postWithin500CharsLimitWithoutAdoptionLink,
postUnicode
post_exceed_500_chars_limit_with_adoption_link,
post_exceed_500_chars_limit_without_adoption_link,
post_within_500_chars_limit_with_adoption_link,
post_within_500_chars_limit_without_adoption_link,
post_unicode
]

def main():
Expand Down
207 changes: 207 additions & 0 deletions manual_testing/mastodon_preview_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
Mastodon formatting preview tool:

This file visualizes different stages of the Mastodon formatting pipeline.

Pipeline structure:
AdoptablePet
-> Post
-> PreparedCaption
-> CaptionThread

Examples:

Preview raw pet input:
python manual_tests/mastodon_preview.py --stage pet

Preview generated platform-independent Post:
python manual_tests/mastodon_preview.py --stage post

Preview fully formatted Mastodon thread:
python manual_tests/mastodon_preview.py --stage debug

Preview only the main Mastodon post:
python manual_tests/mastodon_preview.py --stage main

Preview only reply thread chunks:
python manual_tests/mastodon_preview.py --stage replies

Preview every pipeline stage:
python manual_tests/mastodon_preview.py --stage all

(default behavior is --stage all)

To extend preview stages:
1. Add a new PreviewStage enum entry
2. Add a renderer/action for that stage
"""

from __future__ import annotations

import argparse
import os
import sys
from dataclasses import asdict, is_dataclass
from enum import StrEnum
from pprint import pprint

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from mastodon_manual_test import post_exceed_500_chars_limit_with_adoption_link
from social_posters.mastodon import CaptionThread, MastodonPhase, PosterMastodon
from utils.pipeline import Phase, PipelineResult
from utils.pipeline_preview import PreviewSection, print_section, render_sections


class PreviewStage(StrEnum):
PET = "pet"
POST = "post"
PREPARED_CAPTION = "prepared_caption"
CAPTION_THREAD = "caption_thread"
MAIN = "main"
REPLIES = "replies"
TRACE = "trace"
ALL = "all"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Preview each stage of the Mastodon formatting pipeline."
)
parser.add_argument(
"--stage",
type=PreviewStage,
choices=list(PreviewStage),
default=PreviewStage.ALL,
help="Which construction stage to preview.",
)
return parser.parse_args()


def find_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> Phase | None:
for phase in pipeline.trace:
if phase.name == phase_name:
return phase

return None


def value_for_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> object | None:
phase = find_phase(pipeline, phase_name)
return phase.value if phase else None


def print_value(value: object) -> None:
if is_dataclass(value) and not isinstance(value, type):
pprint(asdict(value))
else:
pprint(value)


def print_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> None:
value = value_for_phase(pipeline, phase_name)

if value is None:
print(f"(No value recorded for {phase_name})")
return

print_value(value)


def print_trace(pipeline: PipelineResult) -> None:
for i, phase in enumerate(pipeline.trace, start=1):
print_section(f"PHASE {i}: {phase.name}")
print_value(phase.value)

if pipeline.errors:
print_section("ERRORS")
for error in pipeline.errors:
print(f"{type(error).__name__}: {error}")


def print_main_caption(thread: CaptionThread | None) -> None:
if thread is None:
print("(No caption thread)")
return

print(thread.main_caption)
print(f"\nLength: {len(thread.main_caption)}")


def print_replies(thread: CaptionThread | None) -> None:
if thread is None:
print("(No caption thread)")
return

if not thread.replies:
print("(No replies)")
return

for i, reply in enumerate(thread.replies, start=1):
print_section(f"REPLY {i}")
print(reply)
print(f"\nLength: {len(reply)}")


def main() -> None:
args = parse_args()
selected_stage: PreviewStage = args.stage

poster = PosterMastodon.__new__(PosterMastodon)

pet = post_exceed_500_chars_limit_with_adoption_link()
pipeline = poster.build_formatting_pipeline(pet)

thread = (
pipeline.value
if pipeline.ok and isinstance(pipeline.value, CaptionThread)
else None
)

sections = [
PreviewSection(
PreviewStage.PET,
"PET",
lambda: print_phase(pipeline, MastodonPhase.PET),
),
PreviewSection(
PreviewStage.POST,
"POST OBJECT",
lambda: print_phase(pipeline, MastodonPhase.POST),
),
PreviewSection(
PreviewStage.PREPARED_CAPTION,
"PREPARED CAPTION",
lambda: print_phase(pipeline, MastodonPhase.PREPARED_CAPTION),
),
PreviewSection(
PreviewStage.CAPTION_THREAD,
"CAPTION THREAD",
lambda: print_phase(pipeline, MastodonPhase.CAPTION_THREAD),
),
PreviewSection(
PreviewStage.MAIN,
"MAIN POST",
lambda: print_main_caption(thread),
),
PreviewSection(
PreviewStage.REPLIES,
"REPLIES",
lambda: print_replies(thread),
),
PreviewSection(
PreviewStage.TRACE,
"FULL TRACE",
lambda: print_trace(pipeline),
),
]

render_sections(
sections=sections,
selected=selected_stage,
all_stage=PreviewStage.ALL,
)


if __name__ == "__main__":
main()
12 changes: 12 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ certifi
chardet==3.0.4
charset-normalizer
clarifai==2.6.2
click==8.3.3
configparser==3.8.1
decorator
EasyProcess==1.1
Expand All @@ -17,14 +18,21 @@ grpcio==1.78.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
hypothesis==6.152.7
idna==2.10
iniconfig==2.3.0
instapy==0.6.16
jsonschema==2.6.0
Mastodon.py
MeaningCloud-python==2.0.0
outcome==1.3.0.post0
packaging==26.2
pip-tools==7.5.3
pluggy==1.6.0
plyer==2.1.0
protobuf==3.20.3
Pygments==2.20.0
pyproject_hooks==1.2.0
PySocks==1.7.1
certifi==2026.4.22
charset-normalizer==3.4.7
Expand All @@ -33,6 +41,10 @@ idna
Mastodon.py==2.2.1
python-dateutil==2.9.0.post0
requests==2.33.1
setuptools==82.0.0
six==1.17.0
sortedcontainers==2.4.0
typing_extensions==4.15.0
urllib3==2.6.3
pytest
wheel==0.47.0
Loading
Loading