pep8speaks: Hello @thomasyu888! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:



* In the file [`tests/test_discussion.py`](https://github.com/Sage-Bionetworks/challengeutils/blob/87be921cde541e55811f5153d077c626cdf7db2d/tests/test_discussion.py):

> [Line 242:10](https://github.com/Sage-Bionetworks/challengeutils/blob/87be921cde541e55811f5153d077c626cdf7db2d/tests/test_discussion.py#L242): [E127](https://duckduckgo.com/?q=pep8%20E127) continuation line over-indented for visual indent
> [Line 244:10](https://github.com/Sage-Bionetworks/challengeutils/blob/87be921cde541e55811f5153d077c626cdf7db2d/tests/test_discussion.py#L244): [E127](https://duckduckgo.com/?q=pep8%20E127) continuation line over-indented for visual indent


diff --git a/challengeutils/discussion.py b/challengeutils/discussion.py
index 1a32828..46b507e 100644
--- a/challengeutils/discussion.py
+++ b/challengeutils/discussion.py
@@ -357,6 +357,23 @@ def create_thread_reply(syn, threadid, message):
 
 
 def copy_thread(syn, thread, project):
+    """Copies a discussion thread and its replies to a project
+
+    Args:
+        syn: synapse object
+        thread: Synapse Thread
+        project: Synapse Project or its id to copy thread to
+
+    Returns:
+        dict: Thread bundle
+    """
+    new_thread_obj = _copy_thread(syn, thread, project)
+    thread_replies = get_thread_replies(syn, thread['id'])
+    for reply in thread_replies:
+        copy_reply(syn, reply, new_thread_obj['id'])
+
+
+def _copy_thread(syn, thread, project):
     """Copies a discussion thread to a project
 
     Args:
@@ -374,8 +391,9 @@ def copy_thread(syn, thread, project):
     on_behalf_of = "On behalf of @{user}\n\n".format(user=username)
     text = get_thread_text(syn, thread['messageKey'])
     new_thread_text = on_behalf_of + text
+    new_thread_obj = create_thread(syn, projectid, title, new_thread_text)
 
-    return create_thread(syn, projectid, title, new_thread_text)
+    return new_thread_obj
 
 
 def copy_reply(syn, reply, thread):
@@ -408,7 +426,4 @@ def copy_forum(syn, project, new_project):
     """
     threads = get_forum_threads(syn, project)
     for thread in threads:
-        new_thread_obj = copy_thread(syn, thread, new_project)
-        thread_replies = get_thread_replies(syn, thread['id'])
-        for reply in thread_replies:
-            copy_reply(syn, reply, new_thread_obj['id'])
+        copy_thread(syn, thread, new_project)
diff --git a/challengeutils/utils.py b/challengeutils/utils.py
index fd71c2b..d10615e 100644
--- a/challengeutils/utils.py
+++ b/challengeutils/utils.py
@@ -244,44 +244,6 @@ def update_all_submissions_annotation_acl(syn, evaluationid, annotations,
         syn.store(new_status)
 
 
-def invite_member_to_team(syn, team, user=None, email=None, message=None):
-    """
-    Invite members to a team
-
-    Args:
-        syn: Synapse object
-        team: Synapse Team id or name
-        user: Synapse username or profile id
-        email: Email of user, do not specify both email and user,
-               but must specify one
-        message: Message for people getting invited to the team
-    """
-    teamid = syn.getTeam(team)['id']
-    is_member = False
-    invite = {'teamId': str(teamid)}
-
-    if email is None:
-        userid = syn.getUserProfile(user)['ownerId']
-        request = \
-            "/team/{teamId}/member/{individualId}/membershipStatus".format(
-                teamId=str(teamid),
-                individualId=str(userid))
-        membership_status = syn.restGET(request)
-        is_member = membership_status['isMember']
-        invite['inviteeId'] = str(userid)
-    else:
-        invite['inviteeEmail'] = email
-
-    if message is not None:
-        invite['message'] = message
-
-    if not is_member:
-        invite = syn.restPOST("/membershipInvitation", body=json.dumps(invite))
-        return invite
-
-    return None
-
-
 def change_submission_status(syn, submissionid, status='RECEIVED'):
     '''
     Function to change a submission status

copy_thread should also copy replies
Currently the logic in copying threads and replies are in copy_forum.  Update copy threads to have copy replies:
```python
import synapseclient
syn = synapseclient.login
discussion_cls = challengeutils.discussion.DiscussionApi(syn)
thread_dict = discussion_cls.get_thread({threadid})
thread_bundle = challengeutils.discussion.copy_thread(syn, thread=thread_dict, project={projectid})
thread_replies = challengeutils.discussion.get_thread_replies(syn, {threadid})
for reply in thread_replies:
    challengeutils.discussion.copy_reply(syn, reply, thread_bundle['id'])
```
diff --git a/tests/test_discussion.py b/tests/test_discussion.py
index 917b231..ad2e42a 100644
--- a/tests/test_discussion.py
+++ b/tests/test_discussion.py
@@ -3,6 +3,7 @@ Test challengeutils.discussion functions
 '''
 import json
 from unittest import mock
+from unittest.mock import patch
 import uuid
 
 import requests
@@ -211,7 +212,7 @@ def test_get_entity_threads():
         assert entity_threads == response
 
 
-def test_copy_thread():
+def test__copy_thread():
     """Tests copying of threads"""
     profile = synapseclient.UserProfile(ownerId="test",
                                         userName="foobar")
@@ -225,7 +226,7 @@ def test_copy_thread():
                            return_value=thread_text) as patch_thread_text,\
          mock.patch.object(discussion, "create_thread",
                            return_value=THREAD_OBJ) as patch_create_thread:
-        thread = discussion.copy_thread(syn, THREAD_OBJ, PROJECTID)
+        thread = discussion._copy_thread(syn, THREAD_OBJ, PROJECTID)
         patch_getuserprofile.assert_called_once_with(THREAD_OBJ['createdBy'])
         patch_thread_text.assert_called_once_with(syn, THREAD_OBJ['messageKey'])
         patch_create_thread.assert_called_once_with(syn, PROJECTID,
@@ -234,6 +235,20 @@ def test_copy_thread():
         assert thread == THREAD_OBJ
 
 
+def test_copy_thread():
+    """Tests copying thread and replies"""
+    with patch.object(discussion, "_copy_thread",
+                      return_value=THREAD_OBJ) as patch_copy_threads,\
+         patch.object(discussion, "get_thread_replies",
+                      return_value=[REPLY_OBJ]) as patch_thread_replies,\
+         patch.object(discussion, "copy_reply") as patch_copy_reply:
+        discussion.copy_thread(syn, THREAD_OBJ, PROJECTID)
+        patch_copy_threads.assert_called_once_with(syn, THREAD_OBJ, PROJECTID)
+        patch_thread_replies.assert_called_once_with(syn, THREAD_OBJ['id'])
+        patch_copy_reply.assert_called_once_with(syn, REPLY_OBJ,
+                                                 THREAD_OBJ['id'])
+
+
 def test_copy_reply():
     """Tests copying of replies"""
     profile = synapseclient.UserProfile(ownerId="test",
@@ -264,14 +279,8 @@ def test_copy_forum():
     with mock.patch.object(discussion, "get_forum_threads",
                            return_value=[THREAD_OBJ]) as patch_get_threads,\
          mock.patch.object(discussion, "copy_thread",
-                           return_value=new_thread) as patch_copy_thread,\
-         mock.patch.object(discussion, "get_thread_replies",
-                           return_value=[REPLY_OBJ]) as patch_thread_replies,\
-         mock.patch.object(discussion, "copy_reply") as patch_copy_reply:
+                           return_value=new_thread) as patch_copy_thread:
         discussion.copy_forum(syn, PROJECTID, new_projectid)
         patch_get_threads.assert_called_once_with(syn, PROJECTID)
         patch_copy_thread.assert_called_once_with(syn, THREAD_OBJ,
                                                   new_projectid)
-        patch_thread_replies.assert_called_once_with(syn, THREAD_OBJ['id'])
-        patch_copy_reply.assert_called_once_with(syn, REPLY_OBJ,
-                                                 new_thread['id'])

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
-e git+https://github.com/Sage-Bionetworks/challengeutils.git@d6b6bcd33637859ca79a14fc56c1f57f1554655f#egg=challengeutils
charset-normalizer==3.4.1
cryptography==44.0.2
Deprecated==1.2.4
entrypoints==0.4
exceptiongroup==1.2.2
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
keyring==12.0.2
keyrings.alt==3.1
numpy==1.21.6
packaging==24.0
pandas==1.0.1
pluggy==1.2.0
pycparser==2.21
pytest==7.4.4
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.31.0
SecretStorage==2.3.1
six==1.17.0
synapseclient==2.0.0
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
wrapt==1.16.0
zipp==3.15.0

name: challengeutils
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - cryptography==44.0.2
      - deprecated==1.2.4
      - entrypoints==0.4
      - exceptiongroup==1.2.2
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - keyring==12.0.2
      - keyrings-alt==3.1
      - numpy==1.21.6
      - packaging==24.0
      - pandas==1.0.1
      - pluggy==1.2.0
      - pycparser==2.21
      - pytest==7.4.4
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.31.0
      - secretstorage==2.3.1
      - six==1.17.0
      - synapseclient==2.0.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - wrapt==1.16.0
      - zipp==3.15.0
prefix: /opt/conda/envs/challengeutils

tests/test_discussion.py::test__copy_thread tests/test_discussion.py::test_copy_thread
tests/test_discussion.py::test__get_project_forum tests/test_discussion.py::test_get_forum_threads tests/test_discussion.py::test_get_thread_replies tests/test_discussion.py::test__get_text tests/test_discussion.py::test_get_thread_text tests/test_discussion.py::test_get_thread_reply_text tests/test_discussion.py::test_get_forum_participants tests/test_discussion.py::test_create_thread tests/test_discussion.py::test_create_thread_reply tests/test_discussion.py::test_get_entity_threads tests/test_discussion.py::test_copy_reply tests/test_discussion.py::test_copy_forum
challengeutils/utils.py challengeutils/discussion.py
challengeutils/utils.py challengeutils/discussion.py