├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .render-buildpacks.json
├── .travis.yml
├── Dockerfile.render
├── LICENSE
├── Procfile
├── README.md
├── fix_authors.py
├── render.yaml
├── requirements.txt
├── runtime.txt
└── sympy_bot
├── __init__.py
├── __main__.py
├── changelog.py
├── submodules.txt
├── tests
├── Release-Notes-for-1.2.md
├── __init__.py
├── test_changelog.py
├── test_get_changelog.py
├── test_update_release_notes.py
├── test_update_wiki.py
├── test_util_functions.py
└── test_webapp.py
├── update_wiki.py
└── webapp.py
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push, pull_request]
3 | jobs:
4 | tests:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | python-version: ['3.9']
9 | fail-fast: false
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-python@v2
13 | with:
14 | python-version: ${{ matrix.python-version }}
15 | - name: Install Dependencies
16 | run: |
17 | pip install pyflakes pytest gidgethub aiohttp requests pytest-aiohttp pytest-mock
18 | - name: Run Tests
19 | run: |
20 | pyflakes .
21 | pytest
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # This file tells git what files to ignore (e.g., you won't see them as
2 | # untracked with "git status"). Add anything to it that can be cleared
3 | # without any worry (e.g., by "git clean -Xdf"), because it can be
4 | # regenerated. Lines beginning with # are comments. You can also ignore
5 | # files on a per-repository basis by modifying the core.excludesfile
6 | # configuration option (see "git help config"). If you need to make git
7 | # track a file that is ignored for some reason, you have to use
8 | # "git add -f". See "git help gitignore" for more information.
9 |
10 |
11 | # Regular Python bytecode file
12 | *.pyc
13 |
14 | # Optimized Python bytecode file
15 | *.pyo
16 |
17 | # Vim's swap files
18 | *.sw[op]
19 |
20 | # Generated files from Jython
21 | *$py.class
22 |
23 | # Generated C files in the polys directory
24 | /sympy/polys/*.c
25 |
26 | # Generated dynamic libraries in the polys directory
27 | /sympy/polys/*.so
28 |
29 | # File generated by setup.py using MANIFEST.in
30 | MANIFEST
31 |
32 | # Generated by ctags (used to improve autocompletion in vim)
33 | tags
34 |
35 | my/
36 |
37 | # Files generated by setup.py
38 | dist/
39 | build/
40 |
41 | # Files generated by setupegg.py
42 | sympy.egg-info/
43 |
44 | # Tox files
45 | tox.ini
46 | .tox/
47 |
48 | # Coverage files (./bin/coverage_report.py)
49 | .coverage
50 | covhtml/
51 |
52 | # Built doc files (cd doc; make html)
53 | doc/_build/
54 | doc/sphinx/
55 |
56 | # pdf files generated from svg files (cd doc; make latex)
57 | doc/src/modules/physics/mechanics/*.pdf
58 | doc/src/modules/physics/vector/*.pdf
59 |
60 | # Mac OS X Junk
61 | .DS_Store
62 |
63 | # Backup files
64 | *~
65 |
66 | # Temp output of sympy/printing/preview.py:
67 | sample.tex
68 |
69 | # IPython Notebook Checkpoints
70 | .ipynb_checkpoints/
71 |
72 | # pytest cache folder
73 | .*cache
74 |
75 | # pytest related data file for slow tests
76 | .ci/durations.log
77 |
--------------------------------------------------------------------------------
/.render-buildpacks.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildpacks": [
3 | "heroku/python"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | sudo: false
4 | python:
5 | - 3.9
6 |
7 | install:
8 | - pip install pyflakes pytest gidgethub aiohttp requests pytest-aiohttp pytest-mock
9 |
10 | script:
11 | - pyflakes .
12 | - pytest
13 |
--------------------------------------------------------------------------------
/Dockerfile.render:
--------------------------------------------------------------------------------
1 | # "v4-" stacks use our new, more rigorous buildpacks management system. They
2 | # allow you to use multiple buildpacks in a single application, as well as to
3 | # use custom buildpacks.
4 | #
5 | # - `v2-` images work with heroku-import v3.x.
6 | # - `v4-` images work with heroku-import v4.x. (We synced the tags.)
7 |
8 | ARG IMPORT_VERSION=v4
9 | ARG HEROKU_STACK=${IMPORT_VERSION}-heroku-20
10 | FROM ghcr.io/renderinc/heroku-app-builder:${HEROKU_STACK} AS builder
11 |
12 |
13 | # Below, please specify any build-time environment variables that you need to
14 | # reference in your build (as called by your buildpacks). If you don't specify
15 | # the arg below, you won't be able to access it in your build. You can also
16 | # specify a default value, as with any Docker `ARG`, if appropriate for your
17 | # use case.
18 |
19 | # ARG MY_BUILD_TIME_ENV_VAR
20 | # ARG DATABASE_URL
21 |
22 | # The FROM statement above refers to an image with the base buildpacks already
23 | # in place. We then run the apply-buildpacks.py script here because, unlike our
24 | # `v2` image, this allows us to expose build-time env vars to your app.
25 | RUN /render/build-scripts/apply-buildpacks.py ${HEROKU_STACK}
26 |
27 | # We strongly recommend that you package a Procfile with your application, but
28 | # if you don't, we'll try to guess one for you. If this is incorrect, please
29 | # add a Procfile that tells us what you need us to run.
30 | RUN if [[ -f /app/Procfile ]]; then \
31 | /render/build-scripts/create-process-types "/app/Procfile"; \
32 | fi;
33 |
34 | # For running the app, we use a clean base image and also one without Ubuntu development packages
35 | # https://devcenter.heroku.com/articles/heroku-20-stack#heroku-20-docker-image
36 | FROM ghcr.io/renderinc/heroku-app-runner:${HEROKU_STACK} AS runner
37 |
38 | # Here we copy your build artifacts from the build image to the runner so that
39 | # the image that we deploy to Render is smaller and, therefore, can start up
40 | # faster.
41 | COPY --from=builder --chown=1000:1000 /render /render/
42 | COPY --from=builder --chown=1000:1000 /app /app/
43 |
44 | # Here we're switching to a non-root user in the container to remove some categories
45 | # of container-escape attack.
46 | USER 1000:1000
47 | WORKDIR /app
48 |
49 | # This sources all /app/.profile.d/*.sh files before process start.
50 | # These are created by buildpacks, and you probably don't have to worry about this.
51 | # https://devcenter.heroku.com/articles/buildpack-api#profile-d-scripts
52 | ENTRYPOINT [ "/render/setup-env" ]
53 |
54 | # 3. By default, we run the 'web' process type defined in the app's Procfile
55 | # You may override the process type that is run by replacing 'web' with another
56 | # process type name in the CMD line below. That process type must have been
57 | # defined in the app's Procfile during build.
58 | CMD [ "/render/process/web" ]
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 SymPy Development Team
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | a. Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 | b. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 | c. Neither the name of SymPy nor the names of its contributors
14 | may be used to endorse or promote products derived from this software
15 | without specific prior written permission.
16 |
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
28 | DAMAGE.
29 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: python3 -m sympy_bot
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SymPy Bot
2 |
3 | This is a GitHub bot for SymPy. It runs on Render and uses the
4 | [@sympy-bot](https://github.com/sympy-bot) GitHub user.
5 |
6 | The bot makes sure that every pull request to SymPy has a release notes entry
7 | in the pull request description. It then automatically adds these notes to the
8 | [release notes](https://github.com/sympy/sympy/wiki/Release-Notes) on the wiki
9 | when the pull request is merged.
10 |
11 | See [the guide on the SymPy
12 | wiki](https://github.com/sympy/sympy/wiki/Writing-Release-Notes) on how to
13 | write release notes.
14 |
15 | The bot may also do other things in the future. If you have any suggestions or
16 | have found any bugs, please open an
17 | [issue](https://github.com/sympy/sympy-bot/issues). Pull requests are welcome
18 | too.
19 |
20 | ## Setting up the bot
21 |
22 | [This tutorial](https://github-bot-tutorial.readthedocs.io/en/latest) is very
23 | good on how to set up Heroku and write GitHub bots. This bot is based on it,
24 | although it uses Render instead of Heroku.
25 |
26 | The bot is tied to Render, so any push to this repo is automatically deployed
27 | there. The Redner
28 | dashboard is at https://dashboard.render.com/
29 |
30 | If the bot stops working, look at the logs on the Render dashboard.
31 |
32 | Next, you need to set up the bot on GitHub. To do so, follow these steps:
33 |
34 | 1. Go to the webhooks settings (for instance, at
35 | https://github.com/sympy/sympy/settings/hooks), and create a new webhook.
36 |
37 | - Set the payload URL to the Render app URL (for instance,
38 | https://sympy-bot.onrender.com/)
39 | - Set the content type to `application/json`
40 | - Generate a random password for the secret. I used the keychain app on my
41 | Mac to generate a 20 character password with random characters. Save this
42 | secret, as you will need to enter it in Render as well.
43 | - Under "Which events would you like to trigger this webhook?" select "Let
44 | me select individual events.". Then make sure only **Pull requests** is
45 | checked.
46 | - Make sure **Active** is checked
47 |
48 | 2. Go to the Render dashboard and click on "Environment" on the left side.
49 | Create two config variables:
50 |
51 | - `GH_SECRET`: set this to the secret you created in step 1 above
52 | - `GH_AUTH`: set this to the personal access token for the `sympy-bot`
53 | user. If you don't have this or need to regenerate it, login as the bot
54 | user and go to the personal access token settings (at
55 | https://github.com/settings/tokens), and create a new token. **VERY
56 | IMPORTANT:** Give the token `public_repo` access only.
57 |
58 | 3. Give the `sympy-bot` user push access to the repo. This is required for the
59 | bot to set commit statuses and to push to the wiki. If you know how to
60 | allow it to do this without giving it as much access, please let me know. I
61 | have tried playing with using reviews instead of statuses, but I couldn't
62 | get it to work.
63 |
64 |
65 | ## Testing
66 |
67 | To test, push to a separate branch (`master` has branch protection) on this
68 | repo (you can also set up a separate testing deploy for your fork if you
69 | want). Then go to the Render dashboard and manually deploy the branch. We may
70 | at some point enable automatic deployments for PRs in Render.
71 |
72 | ### Debugging Webhooks
73 |
74 | To debug webhooks, you can go to the webhooks settings for the repo the bot is
75 | set up on (e.g., https://github.com/sympy/sympy/settings/hooks), and click the
76 | webhook for https://sympy-bot.onrender.com/. This will show you all recent
77 | webhooks that were delivered, with the exact JSON that was delivered as well
78 | as the headers and the response. Each webhook has a corresponding UUID (the
79 | delivery id), which is printed by the bot in the logs when it receives it.
80 |
81 | ## Rate Limits
82 |
83 | GitHub has a rate limit of 5000 requests per hour. A single bot action may
84 | result in multiple API requests. You can see the current rate limit and when
85 | it resets at https://sympy-bot.onrender.com/. If the bot detects that its
86 | rate limits are getting very low, it will post a warning comment on a pull
87 | request. Right now, the bot doesn't use the API very much, so we never get
88 | near the rate limits, unless someone were to attempt to spam it. However, in
89 | the future, this could become an issue if the bot is made to do more stuff.
90 |
--------------------------------------------------------------------------------
/fix_authors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Script to check which pull requests listed in the release notes have multiple
4 | authors. Prior to https://github.com/sympy/sympy-bot/pull/51, the bot was not
5 | posting multiple authors to the wiki correctly, so this script helps to
6 | retroactively fix that.
7 |
8 | This requires doctr for the GitHub login functionality.
9 | """
10 |
11 | import sys
12 | import re
13 |
14 | import requests
15 |
16 | from sympy_bot.changelog import format_authors
17 |
18 | from doctr.local import GitHub_login, GitHub_raise_for_status
19 |
20 | def reauth_GitHub_raise_for_status(r, login_kwargs):
21 | if r.status_code == 401 and r.headers.get('X-GitHub-OTP'):
22 | auth = login_kwargs['auth']
23 | print("You must provide a new 2FA code")
24 | login_kwargs.update(GitHub_login(username=auth.username, password=auth.password))
25 | else:
26 | GitHub_raise_for_status(r)
27 |
28 | def get(url, kwargs):
29 | while True:
30 | r = requests.get(url, **kwargs)
31 | reauth_GitHub_raise_for_status(r, kwargs)
32 | if r.status_code == 401 and r.headers.get('X-GitHub-OTP'):
33 | continue
34 | return r
35 |
36 | def main():
37 | if len(sys.argv) != 2 or sys.argv[1] in ['-h', '--help']:
38 | print("Provide the path to the release notes page you want to fix.")
39 | print("You will need to clone the SymPy wiki repo (git clone git@github.com:sympy/sympy.wiki.git).")
40 | sys.exit(1)
41 |
42 | release_notes_file = sys.argv[1]
43 |
44 | with open(release_notes_file) as f:
45 | release_notes = f.read()
46 |
47 | PRs = set()
48 | for m in re.finditer(r'https://github.com/sympy/sympy/pull/(\d+)', release_notes):
49 | PRs.add(m.group(1))
50 |
51 | login_kwargs = GitHub_login()
52 |
53 | print(f"Found {len(PRs)} PRs, from #{min(PRs, key=int)} to #{max(PRs, key=int)}")
54 |
55 | pr_users = {}
56 | for i, pr in enumerate(sorted(PRs, key=int)):
57 | print(f"Getting PR #{pr}: {i+1}/{len(PRs)}")
58 | pull_request = get(f'https://api.github.com/repos/sympy/sympy/pulls/{pr}', login_kwargs)
59 |
60 | users = set()
61 | commits_url = pull_request.json()['commits_url']
62 | commits = get(commits_url, login_kwargs)
63 | for commit in commits.json():
64 | if commit['author']:
65 | users.add(commit['author']['login'])
66 |
67 | users.add(pull_request.json()['head']['user']['login'])
68 |
69 | pr_users[pr] = users
70 |
71 | for pr in sorted(pr_users, key=int):
72 | users = pr_users[pr]
73 | if len(users) > 1:
74 | authors = format_authors(sorted(users, key=str.lower))
75 | print(f"Fixing authors for #{pr}: {authors}")
76 | release_re = rf'(?m)(\(\[#{pr}\]\(https://github.com/sympy/sympy/pull/{pr}\) by ).*\)$'
77 | repl = rf'\1{authors})'
78 | release_notes, n = re.subn(release_re, repl, release_notes)
79 | if n == 0:
80 | print(f"WARNING: Could not fix the authors for PR #{pr}.")
81 |
82 | print("Updating the release notes file.")
83 | with open(release_notes_file, 'w') as f:
84 | f.write(release_notes)
85 |
86 | if __name__ == '__main__':
87 | sys.exit(main())
88 |
--------------------------------------------------------------------------------
/render.yaml:
--------------------------------------------------------------------------------
1 | # This file was generated by Render's heroku-import Heroku CLI plugin
2 | # https://www.npmjs.com/package/@renderinc/heroku-import
3 | # Schema documented at https://render.com/docs/yaml-spec
4 | services:
5 | - type: web # valid values: https://render.com/docs/yaml-spec#type
6 | name: sympy-bot
7 | env: docker # valid values: https://render.com/docs/yaml-spec#environment
8 | dockerfilePath: Dockerfile.render
9 | plan: free # optional; defaults to starter
10 | numInstances: 1
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | aiohttp
3 | gidgethub
4 |
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.9.16
2 |
--------------------------------------------------------------------------------
/sympy_bot/__init__.py:
--------------------------------------------------------------------------------
1 | from .changelog import (get_valid_headers, get_changelog,
2 | get_release_notes_filename, update_release_notes, format_change)
3 |
4 | __all__ = ['get_valid_headers', 'get_changelog', 'get_release_notes_filename',
5 | 'update_release_notes', 'format_change']
6 |
--------------------------------------------------------------------------------
/sympy_bot/__main__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from aiohttp import web
4 |
5 | from .webapp import main_get, main_post
6 |
7 | if __name__ == "__main__":
8 | app = web.Application()
9 | app.router.add_post("/", main_post)
10 | app.router.add_get("/", main_get)
11 | port = os.environ.get("PORT")
12 | if port is not None:
13 | port = int(port)
14 |
15 | web.run_app(app, port=port)
16 |
--------------------------------------------------------------------------------
/sympy_bot/changelog.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import re
4 | import textwrap
5 | from collections import defaultdict
6 |
7 | PREFIX = '* '
8 | SUFFIX = '{indent}([#{pr_number}](https://github.com/sympy/sympy/pull/{pr_number}) by {authors})\n'
9 | AUTHOR = "[@{author}](https://github.com/{author})"
10 | VERSION_RE = re.compile(r'\d+(?:(?:\.\d+)*(?:\.[1-9]\d*)|\.0)')
11 | BEGIN_RELEASE_NOTES = ""
12 | END_RELEASE_NOTES = ""
13 |
14 | def get_valid_headers():
15 | valid_headers = []
16 | with open(os.path.join(os.path.dirname(__file__), 'submodules.txt')) as f:
17 | for line in f.readlines():
18 | if line and not line.startswith('#'):
19 | valid_headers.append(line.strip())
20 |
21 | return valid_headers
22 |
23 | def is_bullet(line):
24 | l = line.lstrip()
25 | return l.startswith('* ') or l.startswith('- ') or l.startswith('+ ')
26 |
27 | def get_changelog(pr_desc):
28 | """
29 | Parse changelogs from a string
30 |
31 | pr_desc should be a string or list or lines of the pull request
32 | description.
33 |
34 | Returns a tuple (status, message, changelogs), where:
35 |
36 | - status is a boolean indicating if the changelog entry is valid or not
37 | - message is a string with a message to be reported changelogs is a dict
38 | - mapping headers to changelog messages
39 |
40 | """
41 | status = True
42 | message_list = []
43 | changelogs = defaultdict(list)
44 | header = None
45 | if isinstance(pr_desc, (str, bytes)):
46 | pr_desc = pr_desc.splitlines()
47 | lines = iter(pr_desc)
48 | valid_headers = get_valid_headers()
49 |
50 | # First find the release notes header
51 | for line in lines:
52 | if line.strip() == BEGIN_RELEASE_NOTES:
53 | break
54 | else:
55 | return (False, f"* The `{BEGIN_RELEASE_NOTES}` block was not found",
56 | changelogs)
57 |
58 | prefix = ' '
59 | for line in lines:
60 | if not header and not line.strip():
61 | continue
62 | if line.strip() == END_RELEASE_NOTES:
63 | break
64 | if line.strip() == 'NO ENTRY':
65 | message_list += ["* No release notes entry will be added for this pull request."]
66 | changelogs.clear()
67 | status = True
68 | break
69 | elif line.startswith('* ') or line.startswith('- ') or line.startswith('+ '):
70 | header = line.lstrip('*-+ ')
71 | header = header.strip()
72 | if header not in valid_headers:
73 | status = False
74 | if ' ' in header:
75 | # Most likely just forgot the header
76 | message_list += [
77 | "* Release notes must include a header. Please use the submodule name as the header, like",
78 | " ```",
79 | " * core",
80 | " * made Add faster",
81 | "",
82 | " * printing",
83 | " * improve LaTeX printing of fractions",
84 | " ```",
85 | "",
86 | " See [here](https://github.com/sympy/sympy-bot/blob/master/sympy_bot/submodules.txt) for a list of valid release notes headers.",
87 | ]
88 | else:
89 | message_list += [
90 | "* `%s` is not a valid release notes header. Release notes headers should be SymPy submodule names, like" % header,
91 | "",
92 | " ```",
93 | " * core",
94 | " * made Add faster",
95 | "",
96 | " * printing",
97 | " * improve LaTeX printing of fractions",
98 | " ```",
99 | " or `other`.",
100 | "",
101 | " If you have added a new submodule, please add it to the list of valid release notes headers at https://github.com/sympy/sympy-bot/blob/master/sympy_bot/submodules.txt.",
102 | ]
103 |
104 | else:
105 | changelogs[header] # initialize the defaultdict
106 |
107 | else:
108 | if not header:
109 | message_list += [
110 | '* No subheader found. Please add a header for the module, for example,',
111 | '',
112 | ' ```',
113 | ' * solvers',
114 | ' * improve solving of trig equations',
115 | ' ```',
116 | '',
117 | " A list of valid headers can be found at https://github.com/sympy/sympy-bot/blob/master/sympy_bot/submodules.txt.",
118 | ]
119 | status = False
120 | break
121 | elif changelogs[header] and (not line or line.startswith(prefix)) and not is_bullet(line):
122 | # Multiline changelog
123 | len_line_prefix = len(line) - len(line.lstrip())
124 | changelogs[header][-1] += '\n' + ' '*(len_line_prefix - len(prefix)) + line.lstrip()
125 | elif line.strip() and not is_bullet(line):
126 | message_list += [
127 | f'* The line `{line}` does not appear to have a valid Markdown bullet. Make sure it starts with `* `, `- `, or `+ ` with a space after the bullet.',
128 | ]
129 | status = False
130 | break
131 | elif line.strip() and len(line) - len(line.lstrip()) < 2:
132 | message_list += [
133 | f"* The line `{line}` is not indented far enough. Please add at least two spaces before indented bullet points."
134 | ]
135 | status = False
136 | break
137 | else:
138 | prefix = ' '*(len(line) - len(line.lstrip()))
139 | changelogs[header].append(line.strip())
140 | else:
141 | if not changelogs:
142 | message_list += [f'* No release notes were found. Please edit the PR description and write the release notes under `{BEGIN_RELEASE_NOTES}`.']
143 | status = False
144 |
145 | for header in changelogs:
146 | # Clear "empty" changes
147 | changes = []
148 | for change in changelogs[header]:
149 | if change.strip():
150 | changes.append(change.strip())
151 | changelogs[header] = changes
152 | if not changelogs[header]:
153 | message_list += [
154 | '* Invalid release notes entry for `%s`. Make sure it has a release notes entry under it.' % header,
155 | ]
156 | status = False
157 | if not message_list:
158 | if not changelogs:
159 | status = False
160 | message_list = [f'* No release notes were found. Please edit the PR description and write the release notes under `{BEGIN_RELEASE_NOTES}`.']
161 | else:
162 | message_list = ["Your release notes are in good order."]
163 | if not status:
164 | changelogs.clear()
165 | return status, '\n'.join(message_list), changelogs
166 |
167 | def get_release_notes_filename(version):
168 | """
169 | Return filename of release notes for current development version
170 | """
171 | v = VERSION_RE.match(version).group()
172 | return 'Release-Notes-for-' + v + '.md'
173 |
174 | def format_authors(authors):
175 | authors = sorted(authors, key=str.lower)
176 |
177 | if len(authors) == 1:
178 | authors_info = AUTHOR.format(author=authors[0])
179 | elif len(authors) == 2:
180 | authors_info = AUTHOR.format(author=authors[0]) + " and " + AUTHOR.format(author=authors[1])
181 | else:
182 | authors_info = ", ".join([AUTHOR.format(author=author) for author
183 | in authors[:-1]]) + ', and ' + AUTHOR.format(author=authors[-1])
184 |
185 | return authors_info
186 |
187 | def format_change(change, pr_number, authors):
188 | authors_info = format_authors(authors)
189 |
190 | indent = ' '
191 | if '\n\n' in change or ("```" in change and '\n' in change):
192 | change += '\n\n'
193 | indent = ' '
194 |
195 | return textwrap.indent(change + SUFFIX.format(indent=indent,
196 | pr_number=pr_number, authors=authors_info), ' '*len(PREFIX))
197 |
198 | def update_release_notes(*, rel_notes_txt, changelogs, pr_number, authors):
199 | """
200 | Update release notes
201 |
202 | changelogs is assumed to only contain valid headers
203 | """
204 | new_txt = []
205 |
206 | valid_headers = get_valid_headers()
207 | changelogs = changelogs.copy()
208 |
209 | lines = iter(rel_notes_txt.splitlines())
210 | for line in lines:
211 | new_txt.append(line)
212 | if line == "## Changes":
213 | break
214 | else:
215 | raise RuntimeError("The `## Changes` header was not found in the release notes file.")
216 |
217 | for line in lines:
218 | new_txt.append(line)
219 | line_header = line.lstrip(PREFIX)
220 | if line.startswith(PREFIX):
221 | if line_header not in valid_headers:
222 | continue
223 | for header in sorted(changelogs, key=valid_headers.index):
224 | if (valid_headers.index(header) <
225 | valid_headers.index(line_header)):
226 | # We reached a header after one we haven't processed,
227 | # meaning it isn't in the file, so add it.
228 | del new_txt[-1]
229 | new_txt.append(PREFIX + header)
230 | for change in changelogs[header]:
231 | new_txt.append(format_change(change, pr_number, authors))
232 | del changelogs[header]
233 | new_txt.append(line)
234 | continue
235 |
236 | if line_header in changelogs:
237 | for change in changelogs[line_header]:
238 | new_txt.append(format_change(change, pr_number, authors))
239 | del changelogs[line_header]
240 | continue
241 |
242 | if line == "## Authors":
243 | del new_txt[-1]
244 | # Keep the order from submodules.txt
245 | for header in valid_headers:
246 | if header in changelogs:
247 | new_txt.append(PREFIX + header)
248 | for change in changelogs[header]:
249 | new_txt.append(format_change(change, pr_number, authors))
250 | new_txt.append(line)
251 | changelogs.clear()
252 |
253 | new_txt.append('')
254 | if changelogs:
255 | raise RuntimeError("Not all changelog entries were added. Make sure there is a header called `## Authors` at the end of the release notes.")
256 |
257 | return '\n'.join(new_txt)
258 |
--------------------------------------------------------------------------------
/sympy_bot/submodules.txt:
--------------------------------------------------------------------------------
1 | # This is a list of SymPy submodules, which are valid headers for the release
2 | # notes. New top-level modules or major submodules should be added here. Keep
3 | # the entries in alphabetical order, as the order here is the order used in
4 | # the release notes. Each line should have one valid header. Blank lines and
5 | # lines that that start with a # are ignored.
6 |
7 | abc
8 | algebras
9 | assumptions
10 | benchmarks
11 | calculus
12 | categories
13 | codegen
14 | combinatorics
15 | concrete
16 | core
17 | crypto
18 | diffgeom
19 | discrete
20 | external
21 | functions
22 | geometry
23 | holonomic
24 | integrals
25 | interactive
26 | liealgebras
27 | logic
28 | matrices
29 | ntheory
30 | parsing
31 |
32 | # physics is disparate enough to list each submodule separately
33 | physics.biomechanics
34 | physics.continuum_mechanics
35 | physics.control
36 | physics.gaussopt
37 | physics.hep
38 | physics.hydrogen
39 | physics.matrices
40 | physics.mechanics
41 | physics.optics
42 | physics.paulialgebra
43 | physics.pring
44 | physics.qho_1d
45 | physics.quantum
46 | physics.secondquant
47 | physics.sho
48 | physics.units
49 | physics.vector
50 | physics.wigner
51 |
52 | plotting
53 | polys
54 | printing
55 | sandbox
56 | series
57 | sets
58 | simplify
59 | solvers
60 | stats
61 | strategies
62 | tensor
63 | testing
64 | unify
65 | utilities
66 | vector
67 |
68 | # other is a special header for any changes that don't fit into a submodule
69 | other
70 |
--------------------------------------------------------------------------------
/sympy_bot/tests/Release-Notes-for-1.2.md:
--------------------------------------------------------------------------------
1 | These are the release notes for SymPy 1.2.
2 |
3 | SymPy 1.2 was released on Jul 9, 2018.
4 |
5 | This version of SymPy has been tested on Python 2.7, 3.4, 3.5, 3.6, 3.7 and
6 | PyPy. See our [Python version support
7 | policy](https://github.com/sympy/sympy/wiki/Python-version-support-policy) for
8 | more information on when we plan to drop support for older Python versions.
9 |
10 | Install SymPy with
11 |
12 | pip install -U sympy
13 |
14 | or if you use Anaconda
15 |
16 | conda install sympy
17 |
18 | ## Highlights
19 |
20 | There are many changes in 1.2 (see below). Some of the highlights are
21 |
22 | * Python 3.3 is no longer supported. If you require Python 3.3 support, use
23 | SymPy 1.1.1. See our
24 | [policy](https://github.com/sympy/sympy/wiki/Python-version-support-policy)
25 | on dropping support for major Python versions.
26 |
27 | * Experimental LaTeX parsing with `sympy.parsing.latex.parse_latex()` has been
28 | added, based on the `latex2sympy` project. This requires
29 | `antlr-python-runtime` to be installed.
30 | [#13706](https://github.com/sympy/sympy/pull/13706)
31 |
32 | * The vector module has been improved to support orthogonal curvilinear
33 | coordinate systems ([Szymon Mieszczak's GSoC
34 | project](https://github.com/sympy/sympy/wiki/Szymon-Mieszczak,-GSoC-2017-Report,-Implementation-of-multiple-types-of-coordinate-systems-for-vectors))
35 |
36 | * New module `sympy.integrals.intpoly` for integrating uni/bivariate polynomials
37 | over 2-polytopes. ([Arif Ahmed's GSoC
38 | project](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Arif-Ahmed-:-Integration-over-Polytopes))
39 |
40 | * Improvements to the code generation module. ([Björn Dahlgren's GSoC
41 | project](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Bj%C3%B6rn-Dahlgren:-Improved-code-generation-facilities))
42 |
43 | * Improvements to the group theory module. See below for more information.
44 | ([Valeriia Gladkova's GSoC
45 | project](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Valeriia-Gladkova:-Group-Theory))
46 |
47 | * New module `sympy.discrete` for operating on discrete sequences. ([Sidhant Nagpal's GSoC
48 | project](https://github.com/sidhantnagpal/gsoc/wiki/GSoC-2018-Application-Sidhant-Nagpal:-Transforms,-Convolution-&-Linear-Recurrence-Evaluation))
49 |
50 | Other GSoC projects, [Abdullah Javed
51 | Nesar](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Abdullah-Javed-Nesar:-Rule-based-Integrator),
52 | [Arif
53 | Ahmed](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Arif-Ahmed-:-Integration-over-Polytopes),
54 | and [Gaurav Dhingra](https://github.com/gxyd/GSoC-2017-Report) are work in
55 | progress improvements to the `integrate` module (particularly, the new `rubi`
56 | submodule and the `risch` submodule), which are not yet visible to user-facing
57 | code.
58 |
59 | ## Backwards compatibility breaks and deprecations
60 |
61 | * Units: the arguments `dimension` and `scale_factor` in `Quantity` have been
62 | deprecated (see [#14319](https://github.com/sympy/sympy/issues/14319)).
63 |
64 | * Units: Dimensional dependencies are now part of DimensionSystem. All related
65 | methods inside Dimension have been deprecated in favor of the methods in
66 | DimensionSystem (see [#13336](https://github.com/sympy/sympy/issues/13336)).
67 |
68 | * `Matrix.dot` now only works for row and column matrices. Use `*` or `@` to
69 | do matrix multiplication. (see
70 | [#13815](https://github.com/sympy/sympy/issues/13815)).
71 |
72 | * The `k` argument for `line3D.equation()` is deprecated (see
73 | [#13742](https://github.com/sympy/sympy/issues/13742)).
74 |
75 | * `lambdify` no longer automatically wraps arguments with `np.array`, which
76 | improves the performance of calls to lambdified functions. Array wrapping is
77 | necessary if the input involves an operation that would raise an exception
78 | on scalar input, such `lambdify(x, 1/x)(0)`, but these cases are relatively
79 | rare, and wrapping can always be done by the user if necessary.
80 | [#14823](https://github.com/sympy/sympy/pull/14823)
81 |
82 | ## Changes
83 |
84 | * algebras
85 | * add new algebras submodule
86 | * add Quaternion class [#13285](https://github.com/sympy/sympy/pull/13285)
87 |
88 | * calculus
89 | * periodicity: added support for modulo operation
90 | [#13226](https://github.com/sympy/sympy/pull/13226)
91 | * fixed continuous_domain to remove singularities.
92 | [#13335](https://github.com/sympy/sympy/pull/13335)
93 | * Finite Set domains for `function_range`
94 | [#13479](https://github.com/sympy/sympy/pull/13479)
95 | * Fix `function_range` for periodic functions
96 | [#13512](https://github.com/sympy/sympy/pull/13512)
97 |
98 | * combinatorics
99 | * correct gray_to_bin and bin_to_gray
100 | [#14468](https://github.com/sympy/sympy/pull/14468)
101 | * improve the comparison test for convergence
102 | [#14509](https://github.com/sympy/sympy/pull/14509)
103 |
104 | * concrete
105 | * Improve summation of geometric series
106 | [#13991](https://github.com/sympy/sympy/pull/13991)
107 | * add the ratio test to Sum.is_convergent
108 | [#14158](https://github.com/sympy/sympy/pull/14158)
109 | * add the limit comparison test for is_convergent
110 | [#14401](https://github.com/sympy/sympy/pull/14401)
111 | * add bounded times summation convergence test
112 | [#14464](https://github.com/sympy/sympy/pull/14464)
113 | * add factoring to compute certain sums with negative exponents
114 | [#14641](https://github.com/sympy/sympy/pull/14641)
115 |
116 | * core
117 | * Derivatives by a variable a symbolic number of times, like `diff(f(x), (x,
118 | n))` to represent `d^nf/dx^n`, where `n` can be symbolic.
119 | [#13751](https://github.com/sympy/sympy/pull/13751)
120 | [#13892](https://github.com/sympy/sympy/pull/13892)
121 | * various improvements to `Piecewise` and `piecewise_fold`.
122 | [#12951](https://github.com/sympy/sympy/pull/12951),
123 | [#12920](https://github.com/sympy/sympy/pull/12920),
124 | [#13074](https://github.com/sympy/sympy/pull/13074),
125 | [#13282](https://github.com/sympy/sympy/pull/13282),
126 | [#13309](https://github.com/sympy/sympy/pull/13309),
127 | [#13204](https://github.com/sympy/sympy/pull/13204),
128 | [#12587](https://github.com/sympy/sympy/pull/12587),
129 | [#14071](https://github.com/sympy/sympy/pull/14071),
130 | [#14682](https://github.com/sympy/sympy/pull/14682)
131 | * Make Pow.subs not use fractional powers for noncommutative objects
132 | [#13018](https://github.com/sympy/sympy/pull/13018)
133 | * nullary undefined functions (like `f = Function('f'); f()`) are now
134 | allowed [#12977](https://github.com/sympy/sympy/pull/12977)
135 | * add a global option and context manager to disable automatic distribution
136 | (`2*(x + y)` -> `2*x + 2*y`)
137 | [#12440](https://github.com/sympy/sympy/pull/12440)
138 | * make Mul.flatten aware of MatrixExpr
139 | [#13279](https://github.com/sympy/sympy/pull/13279)
140 | * Fix `__rfloordiv__` not being reversed
141 | [#13368](https://github.com/sympy/sympy/pull/13368)
142 | * `Expr` now supports the 3-argument version of the builtin `pow`
143 | [#13364](https://github.com/sympy/sympy/pull/13364)
144 | * Allow undefined functions to be defined with assumptions (like
145 | `Function('f', real=True)`).
146 | [#12945](https://github.com/sympy/sympy/pull/12945)
147 | * Allow Floats to be created from NumPy floats
148 | [#13426](https://github.com/sympy/sympy/pull/13426)
149 | * Fix some incorrect comparisons between numbers
150 | [#13429](https://github.com/sympy/sympy/pull/13429)
151 | * removed x**1.0 == x hack from the core (they are now unequal, remember
152 | that `==` in SymPy means symbolic, not mathematical equality)
153 | [#13518](https://github.com/sympy/sympy/pull/13518)
154 | * Subs substitution now supports derivative by indexed objects in various
155 | cases [#13452](https://github.com/sympy/sympy/pull/13452)
156 | * Add more simplifications for Mod
157 | [#13581](https://github.com/sympy/sympy/pull/13581)
158 | * make `f(1).is_number` give False, where `f = Function('f')`
159 | [#13619](https://github.com/sympy/sympy/pull/13619)
160 | * allow differentiating with respect to tuples, returning an Array type
161 | [#13655](https://github.com/sympy/sympy/pull/13655)
162 | * correct sympify for numpy arrays of size 1
163 | [#13926](https://github.com/sympy/sympy/pull/13926)
164 | * Do not over-simplify rational powers of negative integers
165 | [#13895](https://github.com/sympy/sympy/pull/13895)
166 | * Fix the negative rational powers of negative integers and rationals
167 | [#14024](https://github.com/sympy/sympy/pull/14024)
168 | * improve the performance of modular exponentiation for very large powers
169 | [#14249](https://github.com/sympy/sympy/pull/14249)
170 | * improve mod_inverse for negative numbers
171 | [#14333](https://github.com/sympy/sympy/pull/14333)
172 | * prevent infinite recursion in as_real_imag
173 | [#14404](https://github.com/sympy/sympy/pull/14404)
174 | * remove automatic distribution of infinite factors
175 | [#14383](https://github.com/sympy/sympy/pull/14383)
176 | * fix floor and ceiling of infinite quantities
177 | [#14328](https://github.com/sympy/sympy/pull/14328)
178 | * fix evaluation of negative terms with sympify(evaluate=False)
179 | [#11095](https://github.com/sympy/sympy/issues/11095)
180 | * SymPy numeric expressions now work with `math.trunc`
181 | [#14451](https://github.com/sympy/sympy/pull/14451)
182 | * fix `Mod(x**2, x)` giving 0 (it is not 0 if `x` is fractional)
183 | [#13177](https://github.com/sympy/sympy/pull/13177)
184 | * fix evalf with subs giving incorrect results with `floor`
185 | [#13361](https://github.com/sympy/sympy/pull/13361)
186 | * prevent substitutions in Derivative in cases where it changes the
187 | mathematical meaning of the expression
188 | [#13803](https://github.com/sympy/sympy/pull/13803)
189 | * allow complex powers to be rewritten as exp
190 | [#14712](https://github.com/sympy/sympy/pull/14712)
191 |
192 | * codegen
193 | * Improvements to the code generation module. ([Björn Dahlgren's GSoC
194 | project](https://github.com/sympy/sympy/wiki/GSoC-2017-Report-Bj%C3%B6rn-Dahlgren:-Improved-code-generation-facilities)), including:
195 | * many additions to the sympy.codegen.ast module
196 | * new sympy.codegen rewriting module with codegen related expression
197 | optimizations
198 | * new sympy.codegen.algorithms module with code generatable algorithms using
199 | codegen.ast
200 | * new sympy.codegen.approximations module with code generatable approximations
201 | * add a code printer for GLSL (`glsl_code`)
202 | [#12713](https://github.com/sympy/sympy/pull/12713)
203 | * add support for tensorflow 1.0+
204 | [#13413](https://github.com/sympy/sympy/pull/13413)
205 | * added printer flag so that printer can be passed to codegen()
206 | [#13587](https://github.com/sympy/sympy/pull/13587)
207 | * Add support for fmod in ccode
208 | [#13692](https://github.com/sympy/sympy/pull/13692)
209 | * add more functions to rcode
210 | [#13840](https://github.com/sympy/sympy/pull/13840)
211 | * add more functions to jscode
212 | [#13832](https://github.com/sympy/sympy/pull/13832)
213 | * add support for Max and Min in fcode and octave
214 | [#13903](https://github.com/sympy/sympy/pull/13903)
215 | * fix `pycode()` and 'lambdify' for `Piecewise` functions
216 | [#13969](https://github.com/sympy/sympy/pull/13969)
217 | * fix imaginary number printing in octave
218 | [#13978](https://github.com/sympy/sympy/pull/13978)
219 | * add re, im, arg functions to octave codegen
220 | [#14013](https://github.com/sympy/sympy/pull/14013)
221 | * Fix printing of Fortran code with variables that are the same when
222 | compared case insensitively
223 | [#14003](https://github.com/sympy/sympy/pull/14003)
224 | * Add support for sympy.sign in .printing.pycode
225 | [#14010](https://github.com/sympy/sympy/pull/14010)
226 | * support zoo and -oo in lambdify
227 | [#14306](https://github.com/sympy/sympy/pull/14306)
228 | * lambdify: Improved speed of generated code when unpacking arguments
229 | [#14691](https://github.com/sympy/sympy/pull/14691)
230 | * lambdify now generates actual Python functions rather than lambdas
231 | [#14713](https://github.com/sympy/sympy/pull/14713)
232 | * the `dummify` argument to `lambdify` now by default only converts the
233 | arguments to Dummy when necessary
234 | [#14713](https://github.com/sympy/sympy/pull/14713)
235 | * Add lambdified function source to the linecache. This makes
236 | `inspect.getsource`, IPython's `??`, IPython tracebacks, and most
237 | debuggers show the source code for lambdified functions
238 | [#14739](https://github.com/sympy/sympy/pull/14739)
239 |
240 | * crypto
241 | * Add Goldwasser Micali Encryption
242 | [#13666](https://github.com/sympy/sympy/pull/13666)
243 |
244 | * discrete
245 | * new submodule `sympy.discrete` for operating on discrete sequences with
246 | the following functions:
247 | * `fft`, `ifft` (fast discrete Fourier transforms), `ntt`, `intt` (number theoretic
248 | transform) [#14725](https://github.com/sympy/sympy/pull/14725)
249 | * `convolution` (convolutions on discrete sequences)
250 | [#14745](https://github.com/sympy/sympy/pull/14745), [#14783](https://github.com/sympy/sympy/pull/14783)
251 | * `fwht` and `ifwht` (fast Walsh-Hadamard transform)
252 | [#14765](https://github.com/sympy/sympy/pull/14765)
253 | * Added the ability to evaluate linear recurrences without obtaining
254 | closed form expressions
255 | [#14816](https://github.com/sympy/sympy/pull/14816)
256 |
257 | * functions
258 | * Improve trigonometric function evaluation and logarithmic rewrite
259 | [#13109](https://github.com/sympy/sympy/pull/13109)
260 | * `euler` can now compute Euler polynomials
261 | [#13228](https://github.com/sympy/sympy/pull/13228)
262 | * add some rewrite methods for `sinc`
263 | [#11870](https://github.com/sympy/sympy/pull/11870)
264 | * use gmpy to compute `binomial` if it is installed
265 | [#13394](https://github.com/sympy/sympy/pull/13394)
266 | * fix for B-Splines of higher than 1 degree with repeated knots
267 | [#12214](https://github.com/sympy/sympy/pull/12214)
268 | * allow Min and Max to be rewritten in terms of Abs
269 | [#13614](https://github.com/sympy/sympy/pull/13614)
270 | * add some assumptions for Catalan numbers
271 | [#13628](https://github.com/sympy/sympy/pull/13628)
272 | * Added an interpolating_spline function for symbolic interpolation of
273 | splines [#13829](https://github.com/sympy/sympy/pull/13829)
274 | * Implement more special values of polylogarithm, without exp_polar for s=1
275 | [#13852](https://github.com/sympy/sympy/pull/13852)
276 | * Improved legendre polynomial for negative n
277 | [#13920](https://github.com/sympy/sympy/pull/13920)
278 | * Add evaluation of polygamma(0, z) for all rational z
279 | [#14045](https://github.com/sympy/sympy/pull/14045)
280 | * More general unpolarification for polylog argument
281 | [#14076](https://github.com/sympy/sympy/pull/14076)
282 | * fix `floor` sometimes giving the wrong answer
283 | [#14167](https://github.com/sympy/sympy/pull/14167)
284 | * return zeta(2*n) and zeta(-n) as expressions
285 | [#14178](https://github.com/sympy/sympy/pull/14178)
286 | * automatic log expansion of numbers removed
287 | [#14134](https://github.com/sympy/sympy/pull/14134)
288 | * improve absolute value of complex numbers raised to the power of complex
289 | numbers [#14278](https://github.com/sympy/sympy/pull/14278)
290 | * improve evaluation of inverse trig functions
291 | [#14321](https://github.com/sympy/sympy/pull/14321)
292 | * allow exp to be rewritten as sqrt
293 | [#14358](https://github.com/sympy/sympy/pull/14358)
294 | * evaluate transcendental functions at complex infinity
295 | [#14406](https://github.com/sympy/sympy/pull/14406)
296 | * improve automatic evaluation of reciprocal trig functions
297 | [#14544](https://github.com/sympy/sympy/pull/14544)
298 | * Support added for computing binomial(n, k) where k can be noninteger and n
299 | can be any number. [#14019](https://github.com/sympy/sympy/pull/14019) by
300 | [Yathartha Joshi](https://github.com/Yathartha22)
301 | * Fix binomial(n, k) for negative integer k
302 | [#14575](https://github.com/sympy/sympy/pull/14575)
303 | * Optimize binomial evaluation for integers
304 | [#14576](https://github.com/sympy/sympy/pull/14576)
305 | * implemented partition numbers (`partition()`)
306 | [#14617](https://github.com/sympy/sympy/pull/14617)
307 | * automatically reduce nested floor and ceiling expressions
308 | [#14631](https://github.com/sympy/sympy/pull/14631)
309 | * improve performance Mod of factorial and binomial expressions
310 | [#14636](https://github.com/sympy/sympy/pull/14636)
311 | * improve floor and ceiling rewriting and equality testing
312 | [#14647](https://github.com/sympy/sympy/pull/14647)
313 |
314 | * geometry:
315 | * Second moment and product moment of area of a two-dimensional polygon can
316 | now be computed. [#13939](https://github.com/sympy/sympy/pull/13939) by
317 | [Keshri Kumar Rushyam](https://github.com/rushyam).
318 | * Second moment and product moment of area of an ellipse and a circle can
319 | now be computed. [#14190](https://github.com/sympy/sympy/pull/14190) by
320 | [Keshri Kumar Rushyam](https://github.com/rushyam).
321 | * pairwise intersections.
322 | [#12963](https://github.com/sympy/sympy/pull/12963)
323 | * Added `closing_angle` which represents the angle of one linear entity
324 | relative to another. [#13002](https://github.com/sympy/sympy/pull/13002)
325 | * Added method to compute the exradius of a triangle
326 | [#13318](https://github.com/sympy/sympy/pull/13318)
327 | * Implemented a length property for the Curve class.
328 | [#13328](https://github.com/sympy/sympy/pull/13328)
329 | * make arbitrary_point of Plane run through all points
330 | [#13807](https://github.com/sympy/sympy/pull/13807)
331 | * Remove integral from ellipse circumference calculation
332 | [#14435](https://github.com/sympy/sympy/pull/14435)
333 |
334 | * groups
335 | * It is now possible to check if a finitely presented group is infinite with
336 | `_is_infinite` method (this may return `None` in a number of cases where
337 | the algorithm cannot establish whether a group is infinite). The `order`
338 | method now returns `S.Infinity` for some infinite groups.
339 | ([#12705](https://github.com/sympy/sympy/pull/12705))
340 | * `subgroup` method was added for permutation and finitely presented groups
341 | to return the subgroup generated by given group elements
342 | ([#12658](https://github.com/sympy/sympy/pull/12658)). Subgroups of
343 | finitely presented groups end up having different generators from the
344 | original group but a special class `FpSubgroup` can be used as a
345 | substitute if having the same generators is important.
346 | ([#12827](https://github.com/sympy/sympy/pull/12827))
347 | * The presentation of a finitely presented group can be simplified with
348 | `simplify_presentation`
349 | * Group homomorphisms have been implemented as a `GroupHomomorphism` class
350 | in `sympy.combinatorics.homomorphisms`. A homomorphism can be created
351 | using the function `homomorphism` and additionally one can create special
352 | types of group action homomorphisms for permutation groups with
353 | `block_homomorphism` and `orbit_homomorphism`.
354 | ([#12827](https://github.com/sympy/sympy/pull/12827) and
355 | [#13104](https://github.com/sympy/sympy/pull/13104))
356 | * Rewriting systems for finitely presented groups are implemented. If a
357 | confluent rewriting system is found, this allows to reduce elements to
358 | their normal forms. Otherwise, at least the found reduction rules cab be
359 | used to equate some group elements that otherwise would be treated as
360 | different previously. See
361 | [#12893](https://github.com/sympy/sympy/pull/12893) for more information.
362 | * Convert between permutation and finite presentation of groups by using
363 | `presentation` and `strong_presentation` method of permutation groups
364 | ([#12986](https://github.com/sympy/sympy/pull/12986) and
365 | [#13698](https://github.com/sympy/sympy/pull/13698)) and
366 | `_to_perm_group()` for finitely presented groups
367 | ([#13119](https://github.com/sympy/sympy/pull/13119))
368 | * Compute Sylow subgroups of permutation groups with `sylow_subgroup`
369 | method. ([#13104](https://github.com/sympy/sympy/pull/13104))
370 | * A number of methods and attributes previously only available for
371 | permutation groups now can be used with finite finitely presented groups.
372 | These include `derived_series`, `lower_central_series`, `center`,
373 | `derived_subgroup`, `centralizer`, `normal_closure`, `is_abelian`,
374 | `is_nilpotent`, `is_solvable` and `elements`.
375 | ([#13119](https://github.com/sympy/sympy/pull/13119))
376 |
377 | * holonomic
378 | * Generalize Holonomic to work on symbolic powers
379 | [#12989](https://github.com/sympy/sympy/pull/12989)
380 |
381 | * integrals
382 | * Rewrite Abs and sign as Piecewise for
383 | integration[#13930](https://github.com/sympy/sympy/pull/13930)
384 | * Integrate Max and Min by rewriting them as Piecewise
385 | [#13919](https://github.com/sympy/sympy/pull/13919)
386 | * Add new trigonometric rule in manual integration
387 | [#12651](https://github.com/sympy/sympy/pull/12651)
388 | * Reorder Piecewise output of integration, so that generic answer is first
389 | [#13998](https://github.com/sympy/sympy/pull/13998)
390 | * Add symbolic Riemann sums for definite integrals
391 | [#13988](https://github.com/sympy/sympy/pull/13988)
392 | * In manualintegrate, when integrating by parts, do not use u = non-poly
393 | algebraic [#14015](https://github.com/sympy/sympy/pull/14015)
394 | * fix wrong result integration of rational functions in some cases when
395 | variables have assumptions
396 | [#14082](https://github.com/sympy/sympy/pull/14082)
397 | * simplify some convergence conditions for integrals
398 | [#14092](https://github.com/sympy/sympy/pull/14092)
399 | * add a manualintegrate rule for expanding trig functions
400 | [#14408](https://github.com/sympy/sympy/pull/14408)
401 | * improve manualintegrate for substitutions `u=(a*x+b)**(1/n)`
402 | [#14480](https://github.com/sympy/sympy/pull/14480)
403 | * better handling of manual, meijerg, risch flags in integrate
404 | [#14475](https://github.com/sympy/sympy/pull/14475)
405 | * add `floor` to certain discontinuous integrals
406 | [#13808](https://github.com/sympy/sympy/pull/13808)
407 | * add integration of orthogonal polynomials of general degree
408 | [#14521](https://github.com/sympy/sympy/pull/14521)
409 | * allow integrals with inequalities, such as Integral(x,
410 | x>2) [#14586](https://github.com/sympy/sympy/pull/14586)
411 |
412 | * interpolation
413 | * A spline of given degree that interpolates given data points can be
414 | constructed with `interpolating_spline`.
415 | [#13829](https://github.com/sympy/sympy/pull/13829) by
416 | [Jashan](https://github.com/jashan498).
417 |
418 | * liealgebras
419 | * fix instantiate of CartanType with a string like CartanType('A34')
420 | [#14568](https://github.com/sympy/sympy/pull/14568)
421 |
422 | * matrices
423 | * Computing the Smith Normal Form of a matrix over a ring is now implemented
424 | as the function `smith_normal_form` from `sympy.matrices.normalforms`
425 | ([#12705](https://github.com/sympy/sympy/pull/12705))
426 | * Derivatives for expressions of matrix symbols now experimentally
427 | supported.
428 | * Matrices can be used as deriving variable.
429 | * Matrix norm for order 1 has been added
430 | [#12616](https://github.com/sympy/sympy/pull/12616)
431 | * add MatrixExpr.from_index_summation, which parses expression of matrices
432 | with explicitly summed indices into a matrix expression without indices,
433 | if possible [#13542](https://github.com/sympy/sympy/pull/13542)
434 | * Add functionality for infinity norm of matrices
435 | [#13986](https://github.com/sympy/sympy/pull/13986)
436 | * add numeric check for pivot being zero in Bareiss determinant method
437 | [#13877](https://github.com/sympy/sympy/pull/13877)
438 | * Implement Kronecker product of Matrices
439 | [#14264](https://github.com/sympy/sympy/pull/14264)
440 | * give a simpler representation of exp(M) when M is real
441 | [#14331](https://github.com/sympy/sympy/pull/14331)
442 | * avoid loss of precision in jordan_form
443 | [#13982](https://github.com/sympy/sympy/pull/13982)
444 | * made Mod elementwise for matrices
445 | [#14498](https://github.com/sympy/sympy/pull/14498)
446 | * implement Cholesky and LDL decompositions for Hermitian matrices
447 | [#14474](https://github.com/sympy/sympy/pull/14474)
448 |
449 | * ntheory
450 | * Added totient and mobius range methods to Sieve
451 | [#14628](https://github.com/sympy/sympy/pull/14628)
452 |
453 | * physics.continuum_mechanics
454 | * Added applied_loads and remove_load methods
455 | [#14751](https://github.com/sympy/sympy/pull/14751)
456 | * added method to find point of contraflexure
457 | [#14753](https://github.com/sympy/sympy/pull/14753)
458 | * Added support for composite beams
459 | [#14773](https://github.com/sympy/sympy/pull/14773)
460 | * added `apply_support` and `max_deflection` methods
461 | [#14786](https://github.com/sympy/sympy/pull/14786)
462 |
463 | * physics.mechanics
464 | * add gravity function [#14171](https://github.com/sympy/sympy/pull/14171)
465 |
466 | * physics.optics
467 | * Added transverse_magnification
468 | [#10625](https://github.com/sympy/sympy/pull/10625)
469 | * Implemented Brewster's Angle in Optics Module
470 | [#13756](https://github.com/sympy/sympy/pull/13756)
471 |
472 | * physics.quantum
473 | * Adding support for simplifying powers of tensorproducts
474 | [#13974](https://github.com/sympy/sympy/pull/13974)
475 |
476 | * physics.vector
477 | * fix error pretty printing physics vectors
478 | [#14717](https://github.com/sympy/sympy/pull/14717)
479 |
480 | * physics.wigner
481 | * Make wigner_9j work for certain half-integer argument combinations
482 | [#13602](https://github.com/sympy/sympy/pull/13602)
483 |
484 | * plotting
485 | * Fix plotting of constant functions
486 | [#14023](https://github.com/sympy/sympy/pull/14023)
487 |
488 | * polys
489 | * `degree` now requires a generator to be specified for multivariate
490 | polynomials (use `total_degree` for the total degree).
491 | [#13173](https://github.com/sympy/sympy/pull/13173)
492 | * `total_degree` function added
493 | [#13277](https://github.com/sympy/sympy/pull/13277)
494 | * Allow `itermonomials` to respect non-commutative variables
495 | [#13327](https://github.com/sympy/sympy/pull/13327)
496 | * Improve srepr of polynomial rings and fraction fields
497 | [#13345](https://github.com/sympy/sympy/pull/13345)
498 | * add `Poly.norm`, which computes the conjugates of a polynomial over a
499 | number field. [#13304](https://github.com/sympy/sympy/pull/13304)
500 | * add `srepr` printer for `DMP`
501 | [#13367](https://github.com/sympy/sympy/pull/13367)
502 | * added Finite ring extensions to the `agca` submodule
503 | [#13378](https://github.com/sympy/sympy/pull/13378)
504 | * fix factor for expressions with floating point coefficients
505 | [#13198](https://github.com/sympy/sympy/pull/13198)
506 | * Changed ModularInteger to use fast exponentiation
507 | [#14093](https://github.com/sympy/sympy/pull/14093)
508 | * fix minpoly(I) over domains that contain I
509 | [#14382](https://github.com/sympy/sympy/pull/14382)
510 | * fix the quartic root solver for certain quartic polynomials with complex
511 | coefficients [#14522](https://github.com/sympy/sympy/pull/14522)
512 | * implement Dixons and Macaulay multivariate resultants
513 | [#14370](https://github.com/sympy/sympy/pull/14370)
514 | * support gcd and lcm for compatible irrational numbers
515 | [#14365](https://github.com/sympy/sympy/pull/14365)
516 | * optimized Lagrange Interpolation
517 | [#14603](https://github.com/sympy/sympy/pull/14603)
518 |
519 | * printing
520 | * Latex printer now supports custom printing of logarithmic functions by
521 | newly by accepting `ln_notation` as a boolean (default=`False`) keyword
522 | argument. ([#14180](https://github.com/sympy/sympy/pull/14180))
523 | * Add `mathml` and `cxxcode` to `from sympy import *`.
524 | [#12937](https://github.com/sympy/sympy/pull/12937)
525 | * Add `sympy_integers` option to the str printer to print Rationals in a
526 | SymPy recreatable way. [#13141](https://github.com/sympy/sympy/pull/13141)
527 | * change the set pretty printer to print with {}
528 | [#12087](https://github.com/sympy/sympy/pull/12087)
529 | * fix LaTeX dagger printing
530 | [#13672](https://github.com/sympy/sympy/pull/13672)
531 | * use a better symbol for Equivalent in the pretty printers
532 | [#14105](https://github.com/sympy/sympy/pull/14105)
533 | * fix pretty printing of DiracDelta
534 | [#14104](https://github.com/sympy/sympy/pull/14104)
535 | * change LaTeX printing of O() from `\mathcal{O}` to `\big{O}`
536 | [#14166](https://github.com/sympy/sympy/pull/14166)
537 | * print Poly terms in the correct order in the latex printer
538 | [#14317](https://github.com/sympy/sympy/pull/14317)
539 | * fix latex printing of SeqFormula
540 | [#13971](https://github.com/sympy/sympy/pull/13971)
541 | * add abbreviated printing for units
542 | [#13962](https://github.com/sympy/sympy/pull/13962)
543 | * allow user defined mul_symbol in latex()
544 | [#13798](https://github.com/sympy/sympy/pull/13798)
545 | * add a presentation MathML printer
546 | [#13794](https://github.com/sympy/sympy/pull/13794)
547 | * fix an issue with some missing parentheses from pprint and latex
548 | [#13673](https://github.com/sympy/sympy/pull/13673)
549 | * fix some instances where the str printer would not reuse the same printer
550 | class [#14531](https://github.com/sympy/sympy/pull/14531)
551 | * improve printing of expressions like `Pow(Mul(a,a,evaluate=False), -1,
552 | evaluate=False)` [#14207](https://github.com/sympy/sympy/pull/14207)
553 | * add conjugate to Mathematica printing
554 | [#14109](https://github.com/sympy/sympy/pull/14109)
555 | * the printers no longer dispatch undefined functions like `Function`
556 | subclasses (makes `Function('gamma')` print as `γ` instead of `Γ`)
557 | [#13822](https://github.com/sympy/sympy/pull/13822)
558 | * Make LaTeX printer print full trig names for acsc and asec
559 | [#14774](https://github.com/sympy/sympy/pull/14774)
560 |
561 | * series:
562 | * Bidirectional limits can now be computed by passing dir=+- argument to
563 | `limits.limit` function.
564 | [#11694](https://github.com/sympy/sympy/issue/11694) by [Gaurav
565 | Dhingra](https://github.com/gxyd).
566 | * Evaluate limits of sequences with alternating signs
567 | [#13976](https://github.com/sympy/sympy/pull/13976)
568 |
569 | * sets:
570 | * Ordinals can now be represented in Cantor normal form, and arithmetics can
571 | be performed which includes addition, multiplication, and basic
572 | exponentiation. [#13682](https://github.com/sympy/sympy/pull/13682) by
573 | [Ashish Kumar Gaurav](https://github.com/ashishkg0022)
574 | * SetExpr and arithmetic expressions of sets now supported (via
575 | multimethods). [#14106](https://github.com/sympy/sympy/pull/14106)
576 | * Singleton sets (Reals, Complexes, etc.) can now be accessed without `S`
577 | [#11383](https://github.com/sympy/sympy/pull/11383),
578 | [#12524](https://github.com/sympy/sympy/pull/12524),
579 | [#13572](https://github.com/sympy/sympy/pull/13572),
580 | [#14844](https://github.com/sympy/sympy/pull/14844)
581 | * Add set expressions [#2721](https://github.com/sympy/sympy/pull/2721),
582 | [#14301](https://github.com/sympy/sympy/pull/14301)
583 | * ImageSet now supports multiple sets
584 | [#14145](https://github.com/sympy/sympy/pull/14145)
585 | * be more careful in ConditionSet.subs
586 | [#14564](https://github.com/sympy/sympy/pull/14564)
587 |
588 | * simplify
589 | * improve simplification of Min and Max
590 | [#13054](https://github.com/sympy/sympy/pull/13054),
591 | [#13599](https://github.com/sympy/sympy/pull/13599)
592 | * New function `gammasimp` separated from `combsimp`. `combsimp` does not
593 | apply gamma function simplifications that may make an integer argument
594 | non-integer. Also various improvements to both.
595 | [#13210](https://github.com/sympy/sympy/pull/13210)
596 | * remove unused fu flag from `simplify`.
597 | [#13264](https://github.com/sympy/sympy/pull/13264)
598 | * add `rational` flag to `simplify`.
599 | [#13264](https://github.com/sympy/sympy/pull/13264)
600 | * improved simplification of hyperbolic functions
601 | [#13259](https://github.com/sympy/sympy/pull/13259)
602 | * improve the efficiency of `cse`
603 | [#13221](https://github.com/sympy/sympy/pull/13221)
604 | * Allow cse of unevaluated expressions
605 | [#13271](https://github.com/sympy/sympy/pull/13271)
606 | * fix logcombine for logs of numbers
607 | [#14070](https://github.com/sympy/sympy/pull/14070)
608 | * disable simplification of inverses in simplify() unless the new `inverse`
609 | flag is set [#14422](https://github.com/sympy/sympy/pull/14422)
610 | * `simplify()` now simplifies non-commutative expressions
611 | [#14520](https://github.com/sympy/sympy/pull/14520)
612 |
613 | * solvers
614 | * Enable initial condition solving in `dsolve`
615 | [#11264](https://github.com/sympy/sympy/pull/11264)
616 | * Added support for solving inequalities involving rational functions with
617 | imaginary coefficients [#13296](https://github.com/sympy/sympy/pull/13296)
618 | * the solvers now work with non-Symbols such as Indexed
619 | [#13415](https://github.com/sympy/sympy/pull/13415)
620 | * add failing_assumptions function
621 | [#12147](https://github.com/sympy/sympy/pull/12147)
622 | * Improve the solver for linear 1st order ODE systems of size 2
623 | [#13812](https://github.com/sympy/sympy/pull/13812)
624 | * fix classify_ode(dict=True) for Eq
625 | [#14663](https://github.com/sympy/sympy/pull/14663)
626 | * improve handling of infinite solutions in `solve`
627 | [#14749](https://github.com/sympy/sympy/pull/14749)
628 |
629 | * solveset
630 | * Solveset now supports exponential equations with negative base.
631 | [#13923](https://github.com/sympy/sympy/pull/13923) by [Yathartha
632 | Joshi](https://github.com/Yathartha22)
633 | * Fix `solve_univariate_inequality` for finite set domains
634 | [#13458](https://github.com/sympy/sympy/pull/13458)
635 | * solve more trig equations
636 | [#13941](https://github.com/sympy/sympy/pull/13941)
637 | * improve solveset with piecewise expressions
638 | [#14253](https://github.com/sympy/sympy/pull/14253)
639 | * Improved solveset for negative base exponential equations
640 | [#13844](https://github.com/sympy/sympy/pull/13844)
641 | * allow solving equations with Abs in solveset
642 | [#14449](https://github.com/sympy/sympy/pull/14449)
643 |
644 | * stats
645 | * Added pre-computed cumulative distribution functions to some Random
646 | Symbols [#13284](https://github.com/sympy/sympy/pull/13284),
647 | [#13804](https://github.com/sympy/sympy/pull/13804),
648 | [#13878](https://github.com/sympy/sympy/pull/13878)
649 | * add trapezoidal distribution
650 | [#13419](https://github.com/sympy/sympy/pull/13419)
651 | * The characteristic function of a probability distribution can be computed
652 | with `characteristic_function`.
653 | [#13851](https://github.com/sympy/sympy/pull/13851) and
654 | [#13958](https://github.com/sympy/sympy/pull/13958) by [Ethan
655 | Ward](https://github.com/ethankward).
656 | * Probability of continuous random variables lying in a finite set is now
657 | zero [#14254](https://github.com/sympy/sympy/pull/14254)
658 | * add sampling for GammaInverse distribution
659 | [#14250](https://github.com/sympy/sympy/pull/14250)
660 | * Allow Geometric RVs where `p` is a symbol
661 | [#14472](https://github.com/sympy/sympy/pull/14472)
662 | * various improvements to sampling methods
663 | * fix wrong results with Or conditions
664 | [#14578](https://github.com/sympy/sympy/pull/14578)
665 | * implemented probability for discrete random variables
666 | [#14119](https://github.com/sympy/sympy/pull/14119)
667 | * rename `restricted_domain` to `where` for discrete random variables
668 | [#14604](https://github.com/sympy/sympy/pull/14604)
669 | * implement several missing classes in discrete random variables
670 | [#14218](https://github.com/sympy/sympy/pull/14218)
671 |
672 | * tensor
673 | * do not require assumptions to be set on Idx bounds.
674 | [#12888](https://github.com/sympy/sympy/pull/12888)
675 | * improve `Indexed.free_symbols`
676 | [#13360](https://github.com/sympy/sympy/pull/13360)
677 |
678 | * units
679 | * add SI base units to sympy.physics.units.systems
680 | [#12897](https://github.com/sympy/sympy/pull/12897)
681 | * Implemented get_dimensional_expr for Derivative
682 | [#13003](https://github.com/sympy/sympy/pull/13003)
683 | * Implemented Quantity.get_dimensional_expr() for functions
684 | [#13011](https://github.com/sympy/sympy/pull/13011)
685 | * the relation between base dimensions and derived dimensions are now part
686 | of DimensionSystem, so that they are no longer absolute.
687 | [#13287](https://github.com/sympy/sympy/pull/13287)
688 | * Add Stefan Boltzmann constant
689 | [#13440](https://github.com/sympy/sympy/pull/13440)
690 | * Add Planck charge and derived Planck units
691 | [#13438](https://github.com/sympy/sympy/pull/13438)
692 | * Add katal, gray to derived units
693 | [#13658](https://github.com/sympy/sympy/pull/13658)
694 | * Added derived unit of Radioactivity
695 | [#13839](https://github.com/sympy/sympy/pull/13839)
696 | * fix subs for Quantities [#13855](https://github.com/sympy/sympy/pull/13855)
697 | * add simplification of quantities
698 | [#14286](https://github.com/sympy/sympy/pull/14286)
699 |
700 | * vector
701 | * fix vector + 0 [#14711](https://github.com/sympy/sympy/pull/14711)
702 |
703 | ## Minor changes
704 |
705 | * `free_symbols` and `find_dynamicsymbols` methods now available for vectors
706 | directly, if accompanied by a reference frame argument.
707 | [#13549](https://github.com/sympy/sympy/pull/13549) by [Saloni
708 | Jain](https://github.com/tosalonijain)
709 |
710 | * simplify `Relational.canonical`
711 | [#12906](https://github.com/sympy/sympy/pull/12906)
712 |
713 | * fix `(z**r).is_algebraic` where `z` is algebraic and `r` is rational
714 | [#12924](https://github.com/sympy/sympy/pull/12924)[<8;52;35m
715 |
716 | * Remove `register` keyword from C++17 standard in `cxxcode`
717 | [#12964](https://github.com/sympy/sympy/pull/12964)
718 |
719 | * Allow `autowrap`'s tempdir to be a relative path
720 | [#12944](https://github.com/sympy/sympy/pull/12944)
721 |
722 | * Fix symbols with superscripts raised to a power in the LaTeX printer
723 | [#12894](https://github.com/sympy/sympy/pull/12894)
724 |
725 | * Fix conversion of undefined functions (like `Function('f')`) to Sage.
726 | [#12826](https://github.com/sympy/sympy/pull/12826)
727 |
728 | * Fix wrong result integrating derivatives of multiple variables
729 | [#12971](https://github.com/sympy/sympy/pull/12971)
730 |
731 | * Rich comparison methods now return `NotImplemented` when comparing against
732 | unknown types [#13091](https://github.com/sympy/sympy/pull/13091)
733 |
734 | * Add/Fix logarithmic rewrites of inverse hyperbolic functions
735 | [#13099](https://github.com/sympy/sympy/pull/13099)
736 |
737 | * Fix Mul.expand(deep=False)
738 | [#13281](https://github.com/sympy/sympy/pull/13281)
739 |
740 | * Make DiracDelta(-x) == DiracDelta(x)
741 | [#13308](https://github.com/sympy/sympy/pull/13308)
742 |
743 | * `linsolve` now raises error for non-linear equations
744 | [#13325](https://github.com/sympy/sympy/pull/13325)
745 |
746 | * vlatex: added brackets in multiplication
747 | [#12299](https://github.com/sympy/sympy/pull/12299)
748 |
749 | * Add "The Zen of SymPy" (`import sympy.this`)
750 |
751 | * Install isympy using setuptools, making it available on Windows. isympy can
752 | also now be run as `python -m isympy`
753 | [#13193](https://github.com/sympy/sympy/pull/13193)
754 |
755 | * improve evaluation performance of `binomial`
756 | [#13484](https://github.com/sympy/sympy/pull/13484)
757 |
758 | * Add support for more than two operands in numpy logical and/or
759 | representations in lambdify
760 | [#12608](https://github.com/sympy/sympy/pull/12608)
761 |
762 | * The Mathematica parsing function has been rewritten
763 | [#13533](https://github.com/sympy/sympy/pull/13533)
764 |
765 | * The Mathematica parsing function now has an `additional_translations` flag
766 | [#13544](https://github.com/sympy/sympy/pull/13544)
767 |
768 | * Fix is_real for trigonometric and hyperbolic functions
769 | [#13678](https://github.com/sympy/sympy/pull/13678)
770 |
771 | * @/__matmul__ now only performs matrix multiplication
772 | [#13773](https://github.com/sympy/sympy/pull/13773)
773 |
774 | * Fold expressions with Piecewise before trying to integrate them
775 | [#13866](https://github.com/sympy/sympy/pull/13866)
776 |
777 | * Fix `has` for non-commutative Muls in some cases
778 | [#14026](https://github.com/sympy/sympy/pull/14026)
779 |
780 | * Avoid recursive calls for upper and lower gamma
781 | [#14021](https://github.com/sympy/sympy/pull/14021)
782 |
783 | * Remove the sympy_tokenize module in favor of the standard library tokenize.
784 | Fixes sympification of Python 3-only idioms, like `sympify('α')`.
785 | [#14085](https://github.com/sympy/sympy/pull/14085)
786 |
787 | * Check for logarithmic singularities in `_eval_interval`
788 | [#14097](https://github.com/sympy/sympy/pull/14097)
789 |
790 | * Simple optimization to speed up LCM
791 | [#14314](https://github.com/sympy/sympy/pull/14314)
792 |
793 | * Use extended Euclidean algorithm instead of totient calculation
794 | for modular matrix inverse
795 | [#14347](https://github.com/sympy/sympy/pull/14347)
796 |
797 | ## Authors
798 |
799 | The following people contributed at least one patch to this release (names are
800 | given in alphabetical order by last name). A total of 170 people
801 | contributed to this release. People with a * by their names contributed a
802 | patch for the first time for this release; 124 people contributed
803 | for the first time for this release.
804 |
805 | Thanks to everyone who contributed to this release!
806 |
807 | - Saurabh Agarwal*
808 | - Arif Ahmed
809 | - Jonathan Allan*
810 | - anca-mc*
811 | - Adwait Baokar*
812 | - Nilabja Bhattacharya*
813 | - Johan Blåbäck
814 | - Nicholas Bollweg*
815 | - Francesco Bonazzi
816 | - Matthew Brett
817 | - Marcin Briański
818 | - Bulat*
819 | - Ondřej Čertík
820 | - Arighna Chakrabarty*
821 | - Rishav Chakraborty*
822 | - Ravi charan*
823 | - Lev Chelyadinov*
824 | - Poom Chiarawongse*
825 | - James Cotton*
826 | - cym1*
827 | - czgdp1807*
828 | - Björn Dahlgren
829 | - David Daly*
830 | - der-blaue-elefant*
831 | - Gaurav Dhingra
832 | - dps7ud*
833 | - Rob Drynkin*
834 | - Seth Ebner*
835 | - Peter Enenkel*
836 | - Fredrik Eriksson*
837 | - Boris Ettinger*
838 | - eward*
839 | - Isuru Fernando
840 | - Segev Finer*
841 | - Caley Finn*
842 | - Micah Fitch*
843 | - Lucas Gallindo*
844 | - Bradley Gannon*
845 | - Mauro Garavello
846 | - Varun Garg
847 | - Ashish Kumar Gaurav*
848 | - Jithin D. George*
849 | - Sourav Ghosh*
850 | - Valeriia Gladkova
851 | - Jeremey Gluck*
852 | - Nikoleta Glynatsi*
853 | - Keno Goertz*
854 | - Nityananda Gohain*
855 | - Filip Gokstorp*
856 | - Sayan Goswami*
857 | - Jonathan A. Gross*
858 | - Theodore Han
859 | - Rupesh Harode*
860 | - James Harrop*
861 | - Rahil Hastu*
862 | - helo9*
863 | - Himanshu*
864 | - Hugo*
865 | - David Menéndez Hurtado*
866 | - Ayodeji Ige*
867 | - Itay4*
868 | - Harsh Jain
869 | - Samyak Jain
870 | - Saloni Jain*
871 | - Rohit Jain*
872 | - Shikhar Jaiswal
873 | - Mark Jeromin*
874 | - JMSS-Unknown*
875 | - Yathartha Joshi
876 | - Ishan Joshi*
877 | - Salil Vishnu Kapur*
878 | - Atharva Khare*
879 | - Abhigyan Khaund*
880 | - Sergey B Kirpichev
881 | - Leonid Kovalev
882 | - Amit Kumar
883 | - Jiri Kuncar
884 | - Akash Kundu*
885 | - Himanshu Ladia*
886 | - Rémy Léone*
887 | - Alex Lubbock*
888 | - Jared Lumpe*
889 | - luz.paz*
890 | - Colin B. Macdonald
891 | - Shubham Maheshwari*
892 | - Akshat Maheshwari*
893 | - Shikhar Makhija
894 | - Amit Manchanda*
895 | - Marco Mancini*
896 | - Cezary Marczak*
897 | - Yuki Matsuda*
898 | - Bhautik Mavani
899 | - Maxence Mayrand*
900 | - B McG*
901 | - Cavendish McKay*
902 | - Ehren Metcalfe*
903 | - Aaron Meurer
904 | - Peleg Michaeli
905 | - Szymon Mieszczak
906 | - Aaron Miller*
907 | - Joaquim Monserrat*
908 | - Jason Moore
909 | - Sidhant Nagpal*
910 | - Abdullah Javed Nesar
911 | - Javed Nissar*
912 | - Richard Otis
913 | - Austin Palmer*
914 | - Nikhil Pappu*
915 | - Fermi Paradox
916 | - Rhea Parekh*
917 | - Arihant Parsoya
918 | - Roberto Díaz Pérez*
919 | - Cristian Di Pietrantonio*
920 | - Waldir Pimenta*
921 | - Robert Pollak*
922 | - Alexander Pozdneev*
923 | - P. Sai Prasanth*
924 | - Rastislav Rabatin*
925 | - Zach Raines*
926 | - Shekhar Prasad Rajak
927 | - Juha Remes
928 | - Matthew Rocklin
929 | - Phil Ruffwind
930 | - rushyam*
931 | - Saketh*
932 | - Carl Sandrock*
933 | - Kshitij Saraogi
934 | - Nirmal Sarswat*
935 | - Subhash Saurabh*
936 | - Dmitry Savransky*
937 | - Nico Schlömer
938 | - Stan Schymanski*
939 | - sfoo*
940 | - Ayush Shridhar*
941 | - shruti
942 | - Sartaj Singh
943 | - Rajeev Singh*
944 | - Mayank Singh*
945 | - Arshdeep Singh*
946 | - Jashanpreet Singh*
947 | - Malkhan Singh*
948 | - Gleb Siroki*
949 | - Chris Smith
950 | - Ralf Stephan
951 | - Kalevi Suominen
952 | - Ian Swire*
953 | - symbolique*
954 | - Mihail Tarigradschi*
955 | - James Taylor*
956 | - Pavel Tkachenko*
957 | - Cédric Travelletti*
958 | - Tschijnmo TSCHAU
959 | - Wes Turner*
960 | - Unknown*
961 | - Akash Vaish*
962 | - vedantc98*
963 | - Pauli Virtanen
964 | - Vishal*
965 | - Stewart Wadsworth*
966 | - Ken Wakita*
967 | - Ethan Ward*
968 | - Matthew Wardrop*
969 | - Daniel Wennberg*
970 | - Lucas Wiman*
971 | - Kiyohito Yamazaki*
972 | - Yang Yang*
973 | - Ka Yi
974 | - ylemkimon*
975 | - zhouzq-thu*
976 | - Rob Zinkov*
977 |
--------------------------------------------------------------------------------
/sympy_bot/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sympy/sympy-bot/4f457f788e1a2aeab35ecab3dadf7c508ff8101a/sympy_bot/tests/__init__.py
--------------------------------------------------------------------------------
/sympy_bot/tests/test_changelog.py:
--------------------------------------------------------------------------------
1 | from ..changelog import format_change, format_authors
2 |
3 | def test_format_authors():
4 | assert format_authors(['asmeurer']) == '[@asmeurer](https://github.com/asmeurer)'
5 | assert format_authors(['certik', 'asmeurer']) == '[@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik)'
6 | assert format_authors(['Upabjojr', 'certik', 'asmeurer']) == '[@asmeurer](https://github.com/asmeurer), [@certik](https://github.com/certik), and [@Upabjojr](https://github.com/Upabjojr)'
7 |
8 | def test_format_change():
9 | assert format_change("* improved solvers", 123, ['certik', 'asmeurer']) == ' * improved solvers ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))\n'
10 | assert format_change("* example\n```\n>>> print(1)\n1\n```", 123, ['asmeurer']) == ' * example\n ```\n >>> print(1)\n 1\n ```\n\n ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))\n'
11 | assert format_change('* improved solvers\n\n * dsolve\n * solve', 123, ['asmeurer']) == ' * improved solvers\n\n * dsolve\n * solve\n\n ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))\n'
12 |
13 |
14 | def test_threebackticks_not_multiline():
15 | assert format_change('* added ```rot13``` and ```atbash``` ciphers',
16 | 16516, ['znxftw']) == ' * added ```rot13``` and ```atbash``` ciphers ([#16516](https://github.com/sympy/sympy/pull/16516) by [@znxftw](https://github.com/znxftw))\n'
17 |
--------------------------------------------------------------------------------
/sympy_bot/tests/test_get_changelog.py:
--------------------------------------------------------------------------------
1 | from ..changelog import get_changelog
2 |
3 | def test_no_marker():
4 | desc = """
5 | * solvers
6 | * add a new solver
7 | """
8 |
9 | status, message, changelogs = get_changelog(desc)
10 |
11 | assert not status
12 | assert '' in message
13 | assert not changelogs
14 |
15 | desc = """
16 | NO ENTRY
17 | """
18 |
19 | status, message, changelogs = get_changelog(desc)
20 |
21 | assert not status
22 | assert '' in message
23 | assert not changelogs
24 |
25 | def test_no_entry():
26 | desc = """
27 |
28 | NO ENTRY
29 | """
30 | status, message, changelogs = get_changelog(desc)
31 | assert status
32 | assert "No release notes entry" in message
33 | assert not changelogs
34 |
35 | desc = """
36 |
37 | * solvers
38 | * new solver
39 |
40 | NO ENTRY
41 | """
42 | status, message, changelogs = get_changelog(desc)
43 | assert status
44 | assert "No release notes entry" in message
45 | assert not changelogs
46 |
47 | desc = """
48 |
49 | * solvers
50 |
51 | NO ENTRY
52 | """
53 | status, message, changelogs = get_changelog(desc)
54 | assert status, message
55 | assert "No release notes entry" in message
56 | assert not changelogs
57 |
58 | def test_headers():
59 | desc = """
60 |
61 | * solvers
62 | * new solver
63 |
64 | * core
65 | * faster core
66 | * better stuff
67 |
68 | """
69 | status, message, changelogs = get_changelog(desc)
70 | assert status
71 | assert "good" in message
72 | assert changelogs == {
73 | 'solvers': ['* new solver'],
74 | 'core': ['* faster core', '* better stuff']
75 | }
76 |
77 | def test_bad_headers():
78 | desc = """
79 |
80 | * new solver
81 |
82 | """
83 | status, message, changelogs = get_changelog(desc)
84 | assert not status
85 | assert "subheader" in message
86 | assert not changelogs
87 |
88 | desc = """
89 |
90 | * solvers
91 |
92 | """
93 | status, message, changelogs = get_changelog(desc)
94 | assert not status
95 | assert "invalid" in message.lower()
96 | assert not changelogs
97 |
98 | desc = """
99 |
100 | * new trig solvers
101 |
102 | """
103 | status, message, changelogs = get_changelog(desc)
104 | assert not status
105 | assert "header" in message.lower()
106 | assert not changelogs
107 |
108 | desc = """
109 |
110 | * invalid_header
111 |
112 | """
113 | status, message, changelogs = get_changelog(desc)
114 | assert not status
115 | assert "header" in message.lower()
116 | assert "invalid_header" in message
117 | assert not changelogs
118 |
119 | def test_end_release_marker():
120 |
121 | desc = """
122 |
123 | * solvers
124 | * new trig solvers
125 |
126 | * stuff after
127 | """
128 | status, message, changelogs = get_changelog(desc)
129 | assert status
130 | assert "good" in message
131 | assert changelogs == {'solvers': ['* new trig solvers']}
132 |
133 | desc = """
134 |
135 | * solvers
136 | * new trig solvers
137 |
138 | * core
139 | * not a real change
140 | """
141 | status, message, changelogs = get_changelog(desc)
142 | assert status
143 | assert "good" in message
144 | assert changelogs == {'solvers': ['* new trig solvers']}
145 |
146 | def test_multiline():
147 | desc = """
148 |
149 | * solvers
150 | * new trig solvers
151 |
152 | ```
153 | code
154 | ```
155 | * core
156 | * core change
157 |
158 |
159 | """
160 | status, message, changelogs = get_changelog(desc)
161 | assert status
162 | assert "good" in message
163 | assert changelogs == {
164 | 'solvers': ['* new trig solvers\n\n ```\n code\n ```'],
165 | 'core': ['* core change'],
166 | }
167 |
168 | def test_threebackticks_not_multiline():
169 | desc = """
170 |
171 | * crypto
172 | * added ```rot13``` and ```atbash``` ciphers
173 |
174 | """
175 | status, message, changelogs = get_changelog(desc)
176 | assert status
177 | assert "good" in message
178 | assert changelogs == {
179 | 'crypto': ['* added ```rot13``` and ```atbash``` ciphers'],
180 | }
181 |
182 | def test_multiple_multiline():
183 | # from sympy/sympy#14758, see #14
184 | desc = """
185 |
186 | * parsing
187 | * Added a submodule autolev which can be used to parse Autolev code to SymPy code.
188 | * physics.mechanics
189 | * Added a center of mass function in functions.py which returns the position vector of the center of
190 | mass of a system of bodies.
191 | * Added a corner case check in kane.py (Passes dummy symbols to q_ind and kd_eqs if not passed in
192 | to prevent errors which shouldn't occur).
193 | * physics.vector
194 | * Changed _w_diff_dcm in frame.py to get the correct results.
195 |
196 | """
197 | status, message, changelogs = get_changelog(desc)
198 | assert status
199 | assert "good" in message
200 | assert changelogs == {
201 | 'parsing': [
202 | '* Added a submodule autolev which can be used to parse Autolev code to SymPy code.',
203 | ],
204 | 'physics.mechanics': [
205 | '* Added a center of mass function in functions.py which returns the position vector of the center of\n mass of a system of bodies.',
206 | "* Added a corner case check in kane.py (Passes dummy symbols to q_ind and kd_eqs if not passed in\n to prevent errors which shouldn't occur).",
207 | ],
208 | 'physics.vector': [
209 | "* Changed _w_diff_dcm in frame.py to get the correct results.",
210 | ],
211 | }
212 |
213 | def test_empty_lines():
214 | desc = """
215 |
216 |
217 | * solvers
218 |
219 | * new solver
220 |
221 | * core
222 |
223 | * faster core
224 |
225 | * better stuff
226 |
227 | """
228 | status, message, changelogs = get_changelog(desc)
229 | assert status
230 | assert "good" in message
231 | assert changelogs == {
232 | 'solvers': ['* new solver'],
233 | 'core': ['* faster core', '* better stuff']
234 | }
235 |
236 |
237 | def test_mixed_bullets():
238 | desc = r"""
239 |
240 | - solvers
241 |
242 | - new solver
243 |
244 | + core
245 |
246 | - faster core
247 |
248 | * better stuff
249 |
250 | + improved things
251 |
252 | """
253 | status, message, changelogs = get_changelog(desc)
254 | assert status
255 | assert "good" in message
256 | assert changelogs == {
257 | 'solvers': ['- new solver'],
258 | 'core': ['- faster core', '* better stuff', '+ improved things']
259 | }
260 |
261 | def test_empty():
262 | desc = r"""
263 |
265 |
266 | #### References to other Issues or PRs
267 |
270 |
271 |
272 | #### Brief description of what is fixed or changed
273 |
274 |
275 | #### Other comments
276 |
277 |
278 | #### Release Notes
279 |
280 |
284 |
285 |
286 |
287 |
288 | """
289 | status, message, changelogs = get_changelog(desc)
290 | assert not status
291 | assert 'No release notes were found' in message
292 | assert "" in message
293 | assert not changelogs
294 |
295 | def test_bad_bullet():
296 | desc = r"""
297 |
298 | * core
299 | *`_atomic` can recurse into arguments
300 |
301 | """
302 | status, message, changelogs = get_changelog(desc)
303 | assert not status
304 | assert "*`_atomic` can recurse into arguments" in message
305 | assert "Markdown bullet" in message
306 | assert not changelogs
307 |
308 | def test_bullet_not_indented_far_enough():
309 | desc = r"""
310 |
311 | - solvers
312 |
313 | - new solver
314 | """
315 | status, message, changelogs = get_changelog(desc)
316 | assert not status
317 | assert "indented" in message
318 | assert "- new solver" in message
319 | assert not changelogs
320 |
321 | def test_trailing_whitespace():
322 | desc = """
323 |
324 | - solvers \n\
325 | \n\
326 | - new solver \n\
327 | """
328 |
329 | status, message, changelogs = get_changelog(desc)
330 | assert status, message
331 | assert "good" in message
332 | assert changelogs == {"solvers": ["- new solver"]}
333 |
334 | def test_no_changelog():
335 | desc = """
336 |
337 |
338 |
339 | """
340 | status, message, changelogs = get_changelog(desc)
341 | assert not status
342 | assert "No release notes were found" in message
343 | assert "" in message
344 | assert not changelogs
345 |
346 | desc = """
347 |
348 | """
349 | status, message, changelogs = get_changelog(desc)
350 | assert not status
351 | assert "No release notes were found" in message
352 | assert "" in message
353 | assert not changelogs
354 |
--------------------------------------------------------------------------------
/sympy_bot/tests/test_update_release_notes.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from ..changelog import get_release_notes_filename, update_release_notes
4 |
5 | def test_existing_header():
6 | with open(os.path.join(os.path.dirname(__file__),
7 | get_release_notes_filename('1.2'))) as f:
8 |
9 | notes12 = f.read()
10 |
11 | changelogs = {'solvers': ['* solvers change 1', '* solvers change 2'], 'core': ['- core change 1']}
12 | authors = ['asmeurer', 'certik']
13 | pr_number = '123'
14 |
15 | new_notes12 = update_release_notes(rel_notes_txt=notes12, changelogs=changelogs, pr_number=pr_number, authors=authors)
16 |
17 | assert new_notes12.splitlines()[114:119] == ['', '* core', ' - core change 1 ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))', '', ' * Derivatives by a variable a symbolic number of times, like `diff(f(x), (x,']
18 |
19 | assert new_notes12.splitlines()[614:620] == ['* solvers', ' * solvers change 1 ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))', '', ' * solvers change 2 ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))', '', ' * Enable initial condition solving in `dsolve`']
20 |
21 | def test_new_header():
22 | notes = """\
23 | ## Changes
24 |
25 | ## Authors
26 | """
27 |
28 | # Also makes sure they are inserted in the right order. 'other' should
29 | # stay last in submodules.txt
30 | changelogs = {'solvers': ['- solvers change'], 'other': ['- other changes']}
31 | authors = ['asmeurer']
32 | pr_number = '123'
33 |
34 | new_notes = update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
35 |
36 | assert new_notes == """\
37 | ## Changes
38 |
39 | * solvers
40 | - solvers change ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
41 |
42 | * other
43 | - other changes ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
44 |
45 | ## Authors
46 | """
47 |
48 |
49 | def test_insert_new_header():
50 | notes = """\
51 | ## Changes
52 |
53 | * core
54 | * core change
55 |
56 | * solvers
57 | * solvers change
58 |
59 | ## Authors
60 | """
61 |
62 | # 'other' should stay last in submodules.txt
63 | changelogs = {'sets': ['- sets change'], 'calculus': ['- calculus change'], 'other': ['- other changes']}
64 | authors = ['asmeurer']
65 | pr_number = '123'
66 |
67 | new_notes = update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
68 |
69 | assert new_notes == """\
70 | ## Changes
71 |
72 | * calculus
73 | - calculus change ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
74 |
75 | * core
76 | * core change
77 |
78 | * sets
79 | - sets change ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
80 |
81 | * solvers
82 | * solvers change
83 |
84 | * other
85 | - other changes ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
86 |
87 | ## Authors
88 | """, new_notes
89 |
90 | def test_no_changes_header():
91 | notes = """\
92 |
93 | ## Authors
94 | """
95 |
96 | # Also makes sure they are inserted in the right order. 'other' should
97 | # stay last in submodules.txt
98 | changelogs = {'solvers': ['- solvers change'], 'other': ['- other changes']}
99 | authors = ['asmeurer']
100 | pr_number = '123'
101 |
102 | try:
103 | update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
104 | except RuntimeError as e:
105 | assert "## Changes" in e.args[0]
106 | else:
107 | raise AssertionError("Did not raise")
108 |
109 | notes = ""
110 |
111 | try:
112 | update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
113 | except RuntimeError as e:
114 | assert "## Changes" in e.args[0]
115 | else:
116 | raise AssertionError("Did not raise")
117 |
118 | def test_no_authors_header():
119 | notes = "## Changes"
120 |
121 | changelogs = {'solvers': ['* solvers change 1', '* solvers change 2'], 'core': ['- core change 1']}
122 | authors = ['asmeurer', 'certik']
123 | pr_number = '123'
124 |
125 | try:
126 | update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
127 | except RuntimeError as e:
128 | assert "## Authors" in e.args[0]
129 | else:
130 | raise AssertionError("Did not raise")
131 |
132 | def test_before_changes_header():
133 | # This is based on what happened to the 1.4 release notes (#35), except
134 | # with "## Major Changes" replaced with "## Changes"
135 | notes = """
136 |
137 | ## Backwards compatibility breaks and deprecations
138 |
139 | **Please manually add any backwards compatibility breaks or
140 | [deprecations](https://github.com/sympy/sympy/wiki/Deprecating-policy) here,
141 | in addition to the automatic listing below.**
142 |
143 | * printing
144 | * Old behavior of `theanocode.theano_function` when passed a single-element list as the `outputs` parameter has been deprecated. Use `squeeze=False` to enable the new behavior (the created function will return a single-element list also). To get a function that returns a single value not wrapped in a list, pass a single expression not wrapped in a list to `outputs` (e.g. `theano_function([x, y], x + y)` instead of `theano_function([x, y], [x + y])`). See [#14986](https://github.com/sympy/sympy/issues/14986).
145 |
146 | ## Changes
147 |
148 | * printing
149 | * Added `squeeze=False` option to `theanocode.theano_function` to give more consistent sequence-in-sequence-out behavior, keep old behavior as default for now but deprecate it. ([#14949](https://github.com/sympy/sympy/pull/14949) by [@jlumpe](https://github.com/jlumpe))
150 |
151 | * Added `scalar=True` option to `theanocode.theano_function` to create a function which returns scalars instead of 0-dimensional arrays. ([#14949](https://github.com/sympy/sympy/pull/14949) by [@jlumpe](https://github.com/jlumpe))
152 |
153 | * series
154 | * implemented expression-based recursive sequence class ([#15184](https://github.com/sympy/sympy/pull/15184) by [@rwbogl](https://github.com/rwbogl))
155 |
156 | ## Authors
157 | """
158 |
159 | pr_number = "15207"
160 | authors = ["sylee957"]
161 | changelogs = {'matrices': ["- Added `iszerofunc` parameter for `_eval_det_bareiss()`"]}
162 |
163 | new_notes = update_release_notes(rel_notes_txt=notes, changelogs=changelogs, pr_number=pr_number, authors=authors)
164 |
165 | assert new_notes == """
166 |
167 | ## Backwards compatibility breaks and deprecations
168 |
169 | **Please manually add any backwards compatibility breaks or
170 | [deprecations](https://github.com/sympy/sympy/wiki/Deprecating-policy) here,
171 | in addition to the automatic listing below.**
172 |
173 | * printing
174 | * Old behavior of `theanocode.theano_function` when passed a single-element list as the `outputs` parameter has been deprecated. Use `squeeze=False` to enable the new behavior (the created function will return a single-element list also). To get a function that returns a single value not wrapped in a list, pass a single expression not wrapped in a list to `outputs` (e.g. `theano_function([x, y], x + y)` instead of `theano_function([x, y], [x + y])`). See [#14986](https://github.com/sympy/sympy/issues/14986).
175 |
176 | ## Changes
177 |
178 | * matrices
179 | - Added `iszerofunc` parameter for `_eval_det_bareiss()` ([#15207](https://github.com/sympy/sympy/pull/15207) by [@sylee957](https://github.com/sylee957))
180 |
181 | * printing
182 | * Added `squeeze=False` option to `theanocode.theano_function` to give more consistent sequence-in-sequence-out behavior, keep old behavior as default for now but deprecate it. ([#14949](https://github.com/sympy/sympy/pull/14949) by [@jlumpe](https://github.com/jlumpe))
183 |
184 | * Added `scalar=True` option to `theanocode.theano_function` to create a function which returns scalars instead of 0-dimensional arrays. ([#14949](https://github.com/sympy/sympy/pull/14949) by [@jlumpe](https://github.com/jlumpe))
185 |
186 | * series
187 | * implemented expression-based recursive sequence class ([#15184](https://github.com/sympy/sympy/pull/15184) by [@rwbogl](https://github.com/rwbogl))
188 |
189 | ## Authors
190 | """
191 |
192 |
193 | def test_multiline_indent():
194 | # See test_multiple_multiline() in test_get_changelogs.py and issue #14.
195 | # This is from sympy/sympy#14758.
196 | pr_number = '14758'
197 | authors = ['NikhilPappu']
198 | notes = """\
199 | ## Changes
200 |
201 | ## Authors
202 | """
203 |
204 | changelogs = {
205 | 'parsing': [
206 | '* Added a submodule autolev which can be used to parse Autolev code to SymPy code.',
207 | ],
208 | 'physics.mechanics': [
209 | '* Added a center of mass function in functions.py which returns the position vector of the center of\n mass of a system of bodies.',
210 | "* Added a corner case check in kane.py (Passes dummy symbols to q_ind and kd_eqs if not passed in\n to prevent errors which shouldn't occur).",
211 | ],
212 | 'physics.vector': [
213 | "* Changed _w_diff_dcm in frame.py to get the correct results.",
214 | ],
215 | }
216 |
217 | assert update_release_notes(rel_notes_txt=notes, changelogs=changelogs,
218 | pr_number=pr_number, authors=authors) == """\
219 | ## Changes
220 |
221 | * parsing
222 | * Added a submodule autolev which can be used to parse Autolev code to SymPy code. ([#14758](https://github.com/sympy/sympy/pull/14758) by [@NikhilPappu](https://github.com/NikhilPappu))
223 |
224 | * physics.mechanics
225 | * Added a center of mass function in functions.py which returns the position vector of the center of
226 | mass of a system of bodies. ([#14758](https://github.com/sympy/sympy/pull/14758) by [@NikhilPappu](https://github.com/NikhilPappu))
227 |
228 | * Added a corner case check in kane.py (Passes dummy symbols to q_ind and kd_eqs if not passed in
229 | to prevent errors which shouldn't occur). ([#14758](https://github.com/sympy/sympy/pull/14758) by [@NikhilPappu](https://github.com/NikhilPappu))
230 |
231 | * physics.vector
232 | * Changed _w_diff_dcm in frame.py to get the correct results. ([#14758](https://github.com/sympy/sympy/pull/14758) by [@NikhilPappu](https://github.com/NikhilPappu))
233 |
234 | ## Authors
235 | """
236 |
--------------------------------------------------------------------------------
/sympy_bot/tests/test_update_wiki.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | # This is required for the tests to run properly
5 | import pytest_mock
6 | pytest_mock
7 |
8 | from ..update_wiki import update_wiki, run
9 |
10 | token = "TOKEN"
11 |
12 | TESTING_TOKEN = os.environ.get('TESTING_TOKEN', None)
13 |
14 | def test_run(mocker, capsys):
15 | mocker.patch.dict(os.environ, {"GH_AUTH": token})
16 |
17 | run(['echo', token])
18 | captured = capsys.readouterr()
19 | assert captured.out == 'echo ~~~~~\n~~~~~\n\n'
20 | assert captured.err == ''
21 |
22 | @pytest.mark.skipif(True, reason="The update wiki test needs to be updated. See https://github.com/sympy/sympy-bot/issues/98")
23 | def test_update_wiki(mocker):
24 | mocker.patch.dict(os.environ, {"GH_AUTH": TESTING_TOKEN})
25 |
26 | travis_build = os.environ.get("TRAVIS_JOB_ID")
27 | travis_url = f"https://travis-ci.org/sympy/sympy-bot/builds/{travis_build}" if travis_build else "unknown"
28 | update_wiki(
29 | wiki_url='https://github.com/sympy/sympy-bot.wiki',
30 | release_notes_file='Release-Notes-Tests.md',
31 | changelogs={'other': [f'* Release notes update from {travis_url}']},
32 | pr_number='14942',
33 | authors=['asmeurer'],
34 | )
35 |
--------------------------------------------------------------------------------
/sympy_bot/tests/test_util_functions.py:
--------------------------------------------------------------------------------
1 | from ..changelog import get_release_notes_filename, format_change
2 |
3 | def test_get_release_notes_filename():
4 | assert get_release_notes_filename('1.1') == 'Release-Notes-for-1.1.md'
5 | assert get_release_notes_filename('1.1rc1') == 'Release-Notes-for-1.1.md'
6 | assert get_release_notes_filename('1.1.rc1') == 'Release-Notes-for-1.1.md'
7 | assert get_release_notes_filename('1.1dev') == 'Release-Notes-for-1.1.md'
8 | assert get_release_notes_filename('1.1.dev') == 'Release-Notes-for-1.1.md'
9 |
10 | assert get_release_notes_filename('1.1.1') == 'Release-Notes-for-1.1.1.md'
11 | assert get_release_notes_filename('1.1.1rc1') == 'Release-Notes-for-1.1.1.md'
12 | assert get_release_notes_filename('1.1.1rc1') == 'Release-Notes-for-1.1.1.md'
13 | assert get_release_notes_filename('1.1.1dev') == 'Release-Notes-for-1.1.1.md'
14 | assert get_release_notes_filename('1.1.1.dev') == 'Release-Notes-for-1.1.1.md'
15 |
16 | def test_format_change():
17 | change = '* modified some stuff'
18 | pr_number = '123'
19 | authors = ['asmeurer']
20 | assert format_change(change, pr_number, authors) == \
21 | ' * modified some stuff ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))\n'
22 |
23 | authors = ['asmeurer', 'certik']
24 | assert format_change(change, pr_number, authors) == \
25 | ' * modified some stuff ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))\n'
26 |
27 | authors = ['asmeurer', 'certik', 'sympy']
28 | assert format_change(change, pr_number, authors) == \
29 | ' * modified some stuff ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer), [@certik](https://github.com/certik), and [@sympy](https://github.com/sympy))\n'
30 |
31 | def test_format_change_multiline():
32 | change = '* new trig solvers\n\n ```\n code\n ```'
33 | authors = ['asmeurer']
34 | pr_number = '123'
35 |
36 | assert format_change(change, pr_number, authors) == """\
37 | * new trig solvers
38 |
39 | ```
40 | code
41 | ```
42 |
43 | ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
44 | """
45 |
46 | change = '* new trig solvers\n ```\n code\n ```'
47 | authors = ['asmeurer']
48 | pr_number = '123'
49 |
50 | assert format_change(change, pr_number, authors) == """\
51 | * new trig solvers
52 | ```
53 | code
54 | ```
55 |
56 | ([#123](https://github.com/sympy/sympy/pull/123) by [@asmeurer](https://github.com/asmeurer))
57 | """
58 |
59 | # Make sure changes that aren't really multiline don't get the PR and
60 | # author link on a separate line. From
61 | # https://github.com/sympy/sympy/pull/15133
62 | change = '* Used checksysodesol in test_ode.py to reduce amount of code. Also slightly modified the\n representation of the test cases in the function, but with no changes in their values.'
63 | authors = ['sudz123']
64 | pr_number = '15133'
65 |
66 | assert format_change(change, pr_number, authors) == """\
67 | * Used checksysodesol in test_ode.py to reduce amount of code. Also slightly modified the\n representation of the test cases in the function, but with no changes in their values. ([#15133](https://github.com/sympy/sympy/pull/15133) by [@sudz123](https://github.com/sudz123))
68 | """
69 |
--------------------------------------------------------------------------------
/sympy_bot/tests/test_webapp.py:
--------------------------------------------------------------------------------
1 | """
2 | The tests here test the webapp by sending fake requests through a fake GH
3 | object and checking that the right API calls were made.
4 |
5 | Each fake request has just the API information currently needed by the webapp,
6 | so if more API information is used, it will need to be added.
7 |
8 | The GitHub API docs are useful:
9 |
10 | - Pull request event (the main input to the webapp):
11 | https://developer.github.com/v3/activity/events/types/#pullrequestevent
12 | - Pull request object (the 'pull_request' key to the pull request event):
13 | https://developer.github.com/v3/pulls/
14 | - Commit objects (the output from the 'commits_url'):
15 | https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
16 | - Comment objects (the output from the 'comments_url'):
17 | https://developer.github.com/v3/issues/comments/
18 | - Contents objects (the output from the version_url):
19 | https://developer.github.com/v3/repos/contents/
20 | - Statuses objects (the output from statuses_url):
21 | https://developer.github.com/v3/repos/statuses/
22 |
23 | """
24 |
25 | import datetime
26 | import base64
27 | from subprocess import CalledProcessError
28 | import os
29 |
30 | from gidgethub import sansio
31 |
32 | from ..webapp import router
33 |
34 | # These are required for the tests to run properly
35 | import pytest_aiohttp
36 | pytest_aiohttp
37 | import pytest_mock
38 | pytest_mock
39 |
40 | from pytest import mark, raises
41 | parametrize = mark.parametrize
42 |
43 | class FakeRateLimit:
44 | def __init__(self, *, remaining=5000, limit=5000, reset_datetime=None):
45 | self.remaining = remaining
46 | self.limit = limit
47 | now = datetime.datetime.now(datetime.timezone.utc)
48 | self.reset_datetime = reset_datetime or now + datetime.timedelta(hours=1)
49 |
50 | class FakeGH:
51 | """
52 | Faked gh object
53 |
54 | Arguments:
55 |
56 | - getitem: dictionary mapping {url: result}, or None
57 | - getiter: dictionary mapping {url: result}, or None
58 | - rate_limit: FakeRateLimit object, or None
59 | - post: dictionary mapping {url: result}, or None
60 | - patch: dictionary mapping {url: result}, or None
61 | - delete: dictionary mapping {url: result}, or None
62 |
63 | The results are stored in the properties
64 |
65 | - getiter_urls: list of urls called with getiter
66 | - getitem_urls: list of urls called with getitem
67 | - post_urls: list of urls called with post
68 | - post_data: list of the data input for each post
69 | - patch_urls: list of urls called with patch
70 | - patch_data: list of the data input for each patch
71 | - delete_urls: list of urls called with delete
72 | - rate_limit: the FakeRateLimit object
73 |
74 | Note that GET requests are cached in the code and may be called multiple
75 | times.
76 | """
77 | def __init__(self, *, getitem=None, getiter=None, rate_limit=None,
78 | post=None, patch=None, delete=None):
79 | self._getitem_return = getitem
80 | self._getiter_return = getiter
81 | self._post_return = post
82 | self._patch_return = patch
83 | self._delete_return = delete
84 | self.getiter_urls = []
85 | self.getitem_urls = []
86 | self.post_urls = []
87 | self.post_data = []
88 | self.patch_urls = []
89 | self.patch_data = []
90 | self.delete_urls = []
91 | self.rate_limit = rate_limit or FakeRateLimit()
92 |
93 | async def getitem(self, url):
94 | self.getitem_urls.append(url)
95 | return self._getitem_return[url]
96 |
97 | async def getiter(self, url):
98 | self.getiter_urls.append(url)
99 | for item in self._getiter_return[url]:
100 | yield item
101 |
102 | async def post(self, url, *, data):
103 | self.post_urls.append(url)
104 | self.post_data.append(data)
105 | return self._post_return[url]
106 |
107 | async def patch(self, url, *, data):
108 | self.patch_urls.append(url)
109 | self.patch_data.append(data)
110 | return self._patch_return[url]
111 |
112 | async def delete(self, url):
113 | self.delete_urls.append(url)
114 | return self._delete_return[url]
115 |
116 | def _assert_gh_is_empty(gh):
117 | assert gh._getitem_return == None
118 | assert gh._getiter_return == None
119 | assert gh._post_return == None
120 | assert gh.getiter_urls == []
121 | assert gh.getitem_urls == []
122 | assert gh.post_urls == []
123 | assert gh.post_data == []
124 | assert gh.patch_urls == []
125 | assert gh.patch_data == []
126 | assert gh.delete_urls == []
127 |
128 | def _event(data):
129 | return sansio.Event(data, event='pull_request', delivery_id='1')
130 |
131 | version = '1.2.1'
132 | release_notes_file = 'Release-Notes-for-1.2.1.md'
133 | comments_url = 'https://api.github.com/repos/sympy/sympy/pulls/1/comments'
134 | commits_url = 'https://api.github.com/repos/sympy/sympy/pulls/1/commits'
135 | contents_url = 'https://api.github.com/repos/sympy/sympy/contents/{+path}'
136 | sha = 'a109f824f4cb2b1dd97cf832f329d59da00d609a'
137 | commit_url_template = 'https://api.github.com/repos/sympy/sympy/commits/{sha}'
138 | commit_url = commit_url_template.format(sha=sha)
139 | version_url_template = 'https://api.github.com/repos/sympy/sympy/contents/sympy/release.py?ref={ref}'
140 | version_url = version_url_template.format(ref='master')
141 | html_url = "https://github.com/sympy/sympy"
142 | wiki_url = "https://github.com/sympy/sympy.wiki"
143 | comment_html_url = 'https://github.com/sympy/sympy/pulls/1#issuecomment-1'
144 | comment_html_url2 = 'https://github.com/sympy/sympy/pulls/1#issuecomment-2'
145 | statuses_url = "https://api.github.com/repos/sympy/sympy/statuses/4a09f9f253c7372ec857774b1fe114b1266013fe"
146 | existing_comment_url = "https://api.github.com/repos/sympy/sympy/issues/comments/1"
147 | existing_added_deleted_comment_url = "https://api.github.com/repos/sympy/sympy/issues/comments/2"
148 | pr_number = 1
149 |
150 | valid_PR_description = """
151 |
152 | * solvers
153 | * new trig solvers
154 |
155 | """
156 |
157 | valid_PR_description_no_entry = """
158 |
159 | NO ENTRY
160 |
161 | """
162 |
163 | invalid_PR_description = """
164 |
165 |
166 |
167 | """
168 |
169 | release_notes_comment_body = """\
170 | :white_check_mark:
171 |
172 | Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (version not found!). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).
173 |
174 |
175 |
176 | Your release notes are in good order.
177 |
178 | Here is what the release notes will look like:
179 | * solvers
180 | * new trig solvers ([#1](https://github.com/sympy/sympy/pull/1) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))
181 |
182 | This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.2.1.
183 |
184 | Click here to see the pull request description that was parsed.
185 |
186 |
187 |
188 | * solvers
189 | * new trig solvers
190 |
191 |
192 |
193 |
194 | """
195 |
196 | added_deleted_comment_body = """\
197 | ### \U0001f7e0
198 |
199 | Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (version not found!). I've noticed that some of your commits add or delete files. Since this is sometimes done unintentionally, I wanted to alert you about it.
200 |
201 | This is an experimental feature of SymPy Bot. If you have any feedback on it, please comment at https://github.com/sympy/sympy-bot/issues/75.
202 |
203 | The following commits **add new files**:
204 | * 174b8b37bc33e9eb29e710a233190d02a13bdb54:
205 | - `file1`
206 |
207 | The following commits **delete files**:
208 | * a109f824f4cb2b1dd97cf832f329d59da00d609a:
209 | - `file1`
210 |
211 | If these files were added/deleted on purpose, you can ignore this message.
212 | """
213 |
214 | @parametrize('action', ['closed', 'synchronize', 'edited'])
215 | @parametrize('merged', [True, False])
216 | async def test_no_action_on_closed_prs(action, merged):
217 | if action == 'closed' and merged == True:
218 | return
219 | gh = FakeGH()
220 | event_data = {
221 | 'pull_request': {
222 | 'number': 1,
223 | 'state': 'closed',
224 | 'merged': merged,
225 | },
226 | }
227 | event_data['action'] = action
228 |
229 | event = _event(event_data)
230 |
231 | res = await router.dispatch(event, gh)
232 | assert res is None
233 | _assert_gh_is_empty(gh)
234 |
235 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
236 | async def test_status_good_new_comment(action):
237 | event_data = {
238 | 'pull_request': {
239 | 'number': 1,
240 | 'state': 'open',
241 | 'merged': False,
242 | 'comments_url': comments_url,
243 | 'commits_url': commits_url,
244 | 'head': {
245 | 'user': {
246 | 'login': 'asmeurer',
247 | },
248 | },
249 | 'base': {
250 | 'repo': {
251 | 'contents_url': contents_url,
252 | 'html_url': html_url,
253 | },
254 | 'ref': 'master',
255 | },
256 | 'body': valid_PR_description,
257 | 'statuses_url': statuses_url,
258 | },
259 | 'action': action,
260 | }
261 |
262 |
263 | commits = [
264 | {
265 | 'author': {
266 | 'login': 'asmeurer',
267 | },
268 | 'commit': {
269 | 'message': "A good commit",
270 | },
271 | 'sha': sha,
272 | 'url': commit_url,
273 | },
274 | {
275 | 'author': {
276 | 'login': 'certik',
277 | },
278 | 'commit': {
279 | 'message': "A good commit",
280 | },
281 | 'sha': sha,
282 | 'url': commit_url,
283 | },
284 | # Test commits without a login
285 | {
286 | 'author': None,
287 | 'commit': {
288 | 'message': "A good commit",
289 | },
290 | 'sha': sha,
291 | 'url': commit_url,
292 | },
293 | ]
294 |
295 | commit = {
296 | 'files': [
297 | {
298 | 'status': 'modified',
299 | },
300 | ],
301 | 'parents': [
302 | {
303 | "url": commit_url,
304 | "sha": sha,
305 | },
306 | ],
307 | }
308 |
309 | # No comment from sympy-bot
310 | comments = [
311 | {
312 | 'user': {
313 | 'login': 'asmeurer',
314 | },
315 | },
316 | {
317 | 'user': {
318 | 'login': 'certik',
319 | },
320 | },
321 | ]
322 |
323 | version_file = {
324 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
325 | }
326 |
327 | getiter = {
328 | commits_url: commits,
329 | comments_url: comments,
330 | }
331 |
332 | getitem = {
333 | version_url: version_file,
334 | commit_url: commit,
335 | }
336 | post = {
337 | comments_url: {
338 | 'html_url': comment_html_url,
339 | },
340 | statuses_url: {},
341 | }
342 |
343 | event = _event(event_data)
344 |
345 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
346 |
347 | await router.dispatch(event, gh)
348 |
349 | getitem_urls = gh.getitem_urls
350 | getiter_urls = gh.getiter_urls
351 | post_urls = gh.post_urls
352 | post_data = gh.post_data
353 | patch_urls = gh.patch_urls
354 | patch_data = gh.patch_data
355 |
356 | assert set(getiter_urls) == set(getiter)
357 | assert set(getitem_urls) == set(getitem)
358 | assert post_urls == [comments_url, statuses_url]
359 | assert len(post_data) == 2
360 | # Comments data
361 | assert post_data[0].keys() == {"body"}
362 | comment = post_data[0]["body"]
363 | assert ":white_check_mark:" in comment
364 | assert ":x:" not in comment
365 | assert "new trig solvers" in comment
366 | assert "error" not in comment
367 | assert "https://github.com/sympy/sympy-bot" in comment
368 | for line in valid_PR_description:
369 | assert line in comment
370 | assert "good order" in comment
371 | # Statuses data
372 | assert post_data[1] == {
373 | "state": "success",
374 | "target_url": comment_html_url,
375 | "description": "The release notes look OK",
376 | "context": "sympy-bot/release-notes",
377 | }
378 | assert patch_urls == []
379 | assert patch_data == []
380 |
381 |
382 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
383 | async def test_status_good_existing_comment(action):
384 | event_data = {
385 | 'pull_request': {
386 | 'number': 1,
387 | 'state': 'open',
388 | 'merged': False,
389 | 'comments_url': comments_url,
390 | 'commits_url': commits_url,
391 | 'head': {
392 | 'user': {
393 | 'login': 'asmeurer',
394 | },
395 | },
396 | 'base': {
397 | 'repo': {
398 | 'contents_url': contents_url,
399 | 'html_url': html_url,
400 | },
401 | 'ref': 'master',
402 | },
403 | 'body': valid_PR_description,
404 | 'statuses_url': statuses_url,
405 | },
406 | 'action': action,
407 | }
408 |
409 | commits = [
410 | {
411 | 'author': {
412 | 'login': 'asmeurer',
413 | },
414 | 'commit': {
415 | 'message': "A good commit",
416 | },
417 | 'sha': sha,
418 | 'url': commit_url,
419 | },
420 | {
421 | 'author': {
422 | 'login': 'certik',
423 | },
424 | 'commit': {
425 | 'message': "A good commit",
426 | },
427 | 'sha': sha,
428 | 'url': commit_url,
429 | },
430 | # Test commits without a login
431 | {
432 | 'author': None,
433 | 'commit': {
434 | 'message': "A good commit",
435 | },
436 | 'sha': sha,
437 | 'url': commit_url,
438 | },
439 | ]
440 |
441 | commit = {
442 | 'files': [
443 | {
444 | 'status': 'modified',
445 | },
446 | ],
447 | 'parents': [
448 | {
449 | "url": commit_url,
450 | "sha": sha,
451 | },
452 | ],
453 | }
454 |
455 | # Has comment from sympy-bot
456 | comments = [
457 | {
458 | 'user': {
459 | 'login': 'sympy-bot',
460 | },
461 | 'url': existing_comment_url,
462 | 'body': release_notes_comment_body,
463 | },
464 | {
465 | 'user': {
466 | 'login': 'asmeurer',
467 | },
468 | 'body': "comment",
469 | },
470 | {
471 | 'user': {
472 | 'login': 'certik',
473 | },
474 | "body": "comment",
475 | },
476 | ]
477 |
478 | version_file = {
479 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
480 | }
481 |
482 | getiter = {
483 | commits_url: commits,
484 | comments_url: comments,
485 | }
486 |
487 | getitem = {
488 | version_url: version_file,
489 | commit_url: commit,
490 | }
491 | post = {
492 | statuses_url: {},
493 | }
494 |
495 | patch = {
496 | existing_comment_url: {
497 | 'html_url': comment_html_url,
498 | },
499 | }
500 |
501 | event = _event(event_data)
502 |
503 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
504 |
505 | await router.dispatch(event, gh)
506 |
507 | getitem_urls = gh.getitem_urls
508 | getiter_urls = gh.getiter_urls
509 | post_urls = gh.post_urls
510 | post_data = gh.post_data
511 | patch_urls = gh.patch_urls
512 | patch_data = gh.patch_data
513 |
514 | assert set(getiter_urls) == set(getiter)
515 | assert set(getitem_urls) == set(getitem)
516 | assert post_urls == [statuses_url]
517 | # Statuses data
518 | assert post_data == [{
519 | "state": "success",
520 | "target_url": comment_html_url,
521 | "description": "The release notes look OK",
522 | "context": "sympy-bot/release-notes",
523 | }]
524 | # Comments data
525 | assert patch_urls == [existing_comment_url]
526 | assert len(patch_data) == 1
527 | assert patch_data[0].keys() == {"body"}
528 | comment = patch_data[0]["body"]
529 | assert ":white_check_mark:" in comment
530 | assert ":x:" not in comment
531 | assert "new trig solvers" in comment
532 | assert "error" not in comment
533 | assert "https://github.com/sympy/sympy-bot" in comment
534 | for line in valid_PR_description:
535 | assert line in comment
536 | assert "good order" in comment
537 |
538 |
539 | @parametrize('action', ['closed'])
540 | async def test_closed_with_merging(mocker, action):
541 | # Based on test_status_good_existing_comment
542 |
543 | update_wiki_called_kwargs = {}
544 | def mocked_update_wiki(*args, **kwargs):
545 | nonlocal update_wiki_called_kwargs
546 | assert not args # All args are keyword-only
547 | update_wiki_called_kwargs = kwargs
548 |
549 | mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki)
550 |
551 | event_data = {
552 | 'pull_request': {
553 | 'number': 1,
554 | 'state': 'open',
555 | 'merged': True,
556 | 'comments_url': comments_url,
557 | 'commits_url': commits_url,
558 | 'head': {
559 | 'user': {
560 | 'login': 'asmeurer',
561 | },
562 | },
563 | 'base': {
564 | 'repo': {
565 | 'contents_url': contents_url,
566 | 'html_url': html_url,
567 | },
568 | 'ref': 'master',
569 | },
570 | 'body': valid_PR_description,
571 | 'statuses_url': statuses_url,
572 | },
573 | 'action': action,
574 | }
575 |
576 |
577 | commits = [
578 | {
579 | 'author': {
580 | 'login': 'asmeurer',
581 | },
582 | 'commit': {
583 | 'message': "A good commit",
584 | },
585 | 'sha': sha,
586 | 'url': commit_url,
587 | },
588 | {
589 | 'author': {
590 | 'login': 'certik',
591 | },
592 | 'commit': {
593 | 'message': "A good commit",
594 | },
595 | 'sha': sha,
596 | 'url': commit_url,
597 | },
598 | # Test commits without a login
599 | {
600 | 'author': None,
601 | 'commit': {
602 | 'message': "A good commit",
603 | },
604 | 'sha': sha,
605 | 'url': commit_url,
606 | },
607 | ]
608 |
609 | # Has comment from sympy-bot
610 | comments = [
611 | {
612 | 'user': {
613 | 'login': 'sympy-bot',
614 | },
615 | 'url': existing_comment_url,
616 | 'body': release_notes_comment_body,
617 | },
618 | {
619 | 'user': {
620 | 'login': 'asmeurer',
621 | },
622 | 'body': "comment",
623 | },
624 | {
625 | 'user': {
626 | 'login': 'certik',
627 | },
628 | 'body': "comment",
629 | },
630 | ]
631 |
632 | version_file = {
633 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
634 | }
635 |
636 | getiter = {
637 | commits_url: commits,
638 | comments_url: comments,
639 | }
640 |
641 | getitem = {
642 | version_url: version_file,
643 | }
644 | post = {
645 | statuses_url: {},
646 | }
647 |
648 | patch = {
649 | existing_comment_url: {
650 | 'html_url': comment_html_url,
651 | 'body': release_notes_comment_body,
652 | 'url': existing_comment_url,
653 | },
654 | }
655 |
656 | event = _event(event_data)
657 |
658 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
659 |
660 | await router.dispatch(event, gh)
661 |
662 | getitem_urls = gh.getitem_urls
663 | getiter_urls = gh.getiter_urls
664 | post_urls = gh.post_urls
665 | post_data = gh.post_data
666 | patch_urls = gh.patch_urls
667 | patch_data = gh.patch_data
668 |
669 | assert set(getiter_urls) == set(getiter), getiter_urls
670 | assert set(getitem_urls) == set(getitem)
671 | assert post_urls == [statuses_url]
672 | # Statuses data
673 | assert post_data == [{
674 | "state": "success",
675 | "target_url": comment_html_url,
676 | "description": "The release notes look OK",
677 | "context": "sympy-bot/release-notes",
678 | }]
679 | # Comments data
680 | assert patch_urls == [existing_comment_url, existing_comment_url]
681 | assert len(patch_data) == 2
682 | assert patch_data[0].keys() == {"body"}
683 | comment = patch_data[0]["body"]
684 | assert comment == release_notes_comment_body
685 | assert ":white_check_mark:" in comment
686 | assert ":x:" not in comment
687 | assert "new trig solvers" in comment
688 | assert "error" not in comment
689 | assert "https://github.com/sympy/sympy-bot" in comment
690 | for line in valid_PR_description:
691 | assert line in comment
692 | assert "good order" in comment
693 | updated_comment = patch_data[1]['body']
694 | assert updated_comment.startswith(comment)
695 | assert "have been updated" in updated_comment
696 |
697 | assert update_wiki_called_kwargs == {
698 | 'wiki_url': wiki_url,
699 | 'release_notes_file': release_notes_file,
700 | 'changelogs': {'solvers': ['* new trig solvers']},
701 | 'pr_number': pr_number,
702 | 'authors': ['asmeurer', 'certik'],
703 | }
704 |
705 |
706 |
707 | @parametrize('action', ['closed'])
708 | async def test_closed_with_merging_no_entry(mocker, action):
709 | # Based on test_status_good_existing_comment
710 |
711 | update_wiki_called_kwargs = {}
712 | def mocked_update_wiki(*args, **kwargs):
713 | nonlocal update_wiki_called_kwargs
714 | assert not args # All args are keyword-only
715 | update_wiki_called_kwargs = kwargs
716 |
717 | mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki)
718 |
719 | event_data = {
720 | 'pull_request': {
721 | 'number': 1,
722 | 'state': 'open',
723 | 'merged': True,
724 | 'comments_url': comments_url,
725 | 'commits_url': commits_url,
726 | 'head': {
727 | 'user': {
728 | 'login': 'asmeurer',
729 | },
730 | },
731 | 'base': {
732 | 'repo': {
733 | 'contents_url': contents_url,
734 | 'html_url': html_url,
735 | },
736 | 'ref': 'master',
737 | },
738 | 'body': valid_PR_description_no_entry,
739 | 'statuses_url': statuses_url,
740 | },
741 | 'action': action,
742 | }
743 |
744 | commits = [
745 | {
746 | 'author': {
747 | 'login': 'asmeurer',
748 | },
749 | 'commit': {
750 | 'message': "A good commit",
751 | },
752 | 'sha': sha,
753 | 'url': commit_url,
754 | },
755 | {
756 | 'author': {
757 | 'login': 'certik',
758 | },
759 | 'commit': {
760 | 'message': "A good commit",
761 | },
762 | 'sha': sha,
763 | 'url': commit_url,
764 | },
765 | # Test commits without a login
766 | {
767 | 'author': None,
768 | 'commit': {
769 | 'message': "A good commit",
770 | },
771 | 'sha': sha,
772 | 'url': commit_url,
773 | },
774 | ]
775 |
776 | # Has comment from sympy-bot
777 | comments = [
778 | {
779 | 'user': {
780 | 'login': 'sympy-bot',
781 | },
782 | 'url': existing_comment_url,
783 | 'body': release_notes_comment_body,
784 | },
785 | {
786 | 'user': {
787 | 'login': 'asmeurer',
788 | },
789 | 'body': "comment",
790 | },
791 | {
792 | 'user': {
793 | 'login': 'certik',
794 | },
795 | 'body': "comment",
796 | },
797 | ]
798 |
799 | version_file = {
800 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
801 | }
802 |
803 | getiter = {
804 | commits_url: commits,
805 | comments_url: comments,
806 | }
807 |
808 | getitem = {
809 | version_url: version_file,
810 | }
811 | post = {
812 | statuses_url: {},
813 | }
814 |
815 | patch = {
816 | existing_comment_url: {
817 | 'html_url': comment_html_url,
818 | 'body': release_notes_comment_body,
819 | 'url': existing_comment_url,
820 | },
821 | }
822 |
823 | event = _event(event_data)
824 |
825 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
826 |
827 | await router.dispatch(event, gh)
828 |
829 | getitem_urls = gh.getitem_urls
830 | getiter_urls = gh.getiter_urls
831 | post_urls = gh.post_urls
832 | post_data = gh.post_data
833 | patch_urls = gh.patch_urls
834 | patch_data = gh.patch_data
835 |
836 | assert set(getiter_urls) == set(getiter), getiter_urls
837 | assert set(getitem_urls) == set(getitem)
838 | assert post_urls == [statuses_url]
839 | # Statuses data
840 | assert post_data == [{
841 | "state": "success",
842 | "target_url": comment_html_url,
843 | "description": "The release notes look OK",
844 | "context": "sympy-bot/release-notes",
845 | }]
846 | # Comments data
847 | assert patch_urls == [existing_comment_url]
848 | assert len(patch_data) == 1
849 | assert patch_data[0].keys() == {"body"}
850 | comment = patch_data[0]["body"]
851 | assert "No release notes entry will be added for this pull request." in comment
852 | assert ":white_check_mark:" in comment
853 | assert ":x:" not in comment
854 | assert "error" not in comment
855 | assert "https://github.com/sympy/sympy-bot" in comment
856 | for line in valid_PR_description:
857 | assert line in comment
858 |
859 | assert update_wiki_called_kwargs == {}
860 |
861 | @parametrize('action', ['closed'])
862 | @parametrize('exception', [RuntimeError('error message'),
863 | CalledProcessError(1, 'cmd')])
864 | async def test_closed_with_merging_update_wiki_error(mocker, action, exception):
865 | # Based on test_closed_with_merging
866 |
867 | update_wiki_called_kwargs = {}
868 | def mocked_update_wiki(*args, **kwargs):
869 | nonlocal update_wiki_called_kwargs
870 | assert not args # All args are keyword-only
871 | update_wiki_called_kwargs = kwargs
872 | raise exception
873 |
874 | mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki)
875 | mocker.patch.dict(os.environ, {"GH_AUTH": "TESTING TOKEN"})
876 |
877 | event_data = {
878 | 'pull_request': {
879 | 'number': 1,
880 | 'state': 'open',
881 | 'merged': True,
882 | 'comments_url': comments_url,
883 | 'commits_url': commits_url,
884 | 'head': {
885 | 'user': {
886 | 'login': 'asmeurer',
887 | },
888 | },
889 | 'base': {
890 | 'repo': {
891 | 'contents_url': contents_url,
892 | 'html_url': html_url,
893 | },
894 | 'ref': 'master',
895 | },
896 | 'body': valid_PR_description,
897 | 'statuses_url': statuses_url,
898 | },
899 | 'action': action,
900 | }
901 |
902 |
903 | commits = [
904 | {
905 | 'author': {
906 | 'login': 'asmeurer',
907 | },
908 | 'commit': {
909 | 'message': "A good commit",
910 | },
911 | 'sha': sha,
912 | 'url': commit_url,
913 | },
914 | {
915 | 'author': {
916 | 'login': 'certik',
917 | },
918 | 'commit': {
919 | 'message': "A good commit",
920 | },
921 | 'sha': sha,
922 | 'url': commit_url,
923 | },
924 | # Test commits without a login
925 | {
926 | 'author': None,
927 | 'commit': {
928 | 'message': "A good commit",
929 | },
930 | 'sha': sha,
931 | 'url': commit_url,
932 | },
933 | ]
934 |
935 | # Has comment from sympy-bot
936 | comments = [
937 | {
938 | 'user': {
939 | 'login': 'sympy-bot',
940 | },
941 | 'url': existing_comment_url,
942 | 'body': release_notes_comment_body,
943 | },
944 | {
945 | 'user': {
946 | 'login': 'asmeurer',
947 | },
948 | 'body': "comment",
949 | },
950 | {
951 | 'user': {
952 | 'login': 'certik',
953 | },
954 | 'body': "comment",
955 | },
956 | ]
957 |
958 | version_file = {
959 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
960 | }
961 |
962 | getiter = {
963 | commits_url: commits,
964 | comments_url: comments,
965 | }
966 |
967 | getitem = {
968 | version_url: version_file,
969 | }
970 | post = {
971 | statuses_url: {},
972 | comments_url: {
973 | 'html_url': comment_html_url,
974 | },
975 | }
976 |
977 | patch = {
978 | existing_comment_url: {
979 | 'html_url': comment_html_url,
980 | 'body': release_notes_comment_body,
981 | 'url': existing_comment_url,
982 | },
983 | }
984 |
985 | event = _event(event_data)
986 |
987 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
988 |
989 | with raises(type(exception)):
990 | await router.dispatch(event, gh)
991 |
992 | getitem_urls = gh.getitem_urls
993 | getiter_urls = gh.getiter_urls
994 | post_urls = gh.post_urls
995 | post_data = gh.post_data
996 | patch_urls = gh.patch_urls
997 | patch_data = gh.patch_data
998 |
999 | assert set(getiter_urls) == set(getiter), getiter_urls
1000 | assert set(getitem_urls) == set(getitem)
1001 | assert post_urls == [statuses_url, comments_url, statuses_url]
1002 | # Statuses data
1003 | assert len(post_data) == 3
1004 | assert post_data[0] == {
1005 | "state": "success",
1006 | "target_url": comment_html_url,
1007 | "description": "The release notes look OK",
1008 | "context": "sympy-bot/release-notes",
1009 | }
1010 | assert post_data[1].keys() == {'body'}
1011 | error_message = post_data[1]['body']
1012 | assert ':rotating_light:' in error_message
1013 | assert 'ERROR' in error_message
1014 | assert 'https://github.com/sympy/sympy-bot/issues' in error_message
1015 | if isinstance(exception, RuntimeError):
1016 | assert 'error message' in error_message
1017 | else:
1018 | assert "Command 'cmd' returned non-zero exit status 1." in error_message
1019 | assert post_data[2] == {
1020 | "state": "error",
1021 | "target_url": comment_html_url,
1022 | "description": "There was an error updating the release notes on the wiki.",
1023 | "context": "sympy-bot/release-notes",
1024 | }
1025 | # Comments data
1026 | assert patch_urls == [existing_comment_url]
1027 | assert len(patch_data) == 1
1028 | assert patch_data[0].keys() == {"body"}
1029 | comment = patch_data[0]["body"]
1030 | assert comment == release_notes_comment_body
1031 | assert ":white_check_mark:" in comment
1032 | assert ":x:" not in comment
1033 | assert "new trig solvers" in comment
1034 | assert "error" not in comment
1035 | assert "https://github.com/sympy/sympy-bot" in comment
1036 | for line in valid_PR_description:
1037 | assert line in comment
1038 | assert "good order" in comment
1039 |
1040 | assert update_wiki_called_kwargs == {
1041 | 'wiki_url': wiki_url,
1042 | 'release_notes_file': release_notes_file,
1043 | 'changelogs': {'solvers': ['* new trig solvers']},
1044 | 'pr_number': pr_number,
1045 | 'authors': ['asmeurer', 'certik'],
1046 | }
1047 |
1048 |
1049 |
1050 | @parametrize('action', ['closed'])
1051 | async def test_closed_with_merging_bad_status_error(mocker, action):
1052 | # Based on test_closed_with_merging
1053 |
1054 | update_wiki_called_kwargs = {}
1055 | def mocked_update_wiki(*args, **kwargs):
1056 | nonlocal update_wiki_called_kwargs
1057 | assert not args # All args are keyword-only
1058 | update_wiki_called_kwargs = kwargs
1059 |
1060 | mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki)
1061 | mocker.patch.dict(os.environ, {"GH_AUTH": "TESTING TOKEN"})
1062 |
1063 | event_data = {
1064 | 'pull_request': {
1065 | 'number': 1,
1066 | 'state': 'open',
1067 | 'merged': True,
1068 | 'comments_url': comments_url,
1069 | 'commits_url': commits_url,
1070 | 'head': {
1071 | 'user': {
1072 | 'login': 'asmeurer',
1073 | },
1074 | },
1075 | 'base': {
1076 | 'repo': {
1077 | 'contents_url': contents_url,
1078 | 'html_url': html_url,
1079 | },
1080 | 'ref': 'master',
1081 | },
1082 | 'body': invalid_PR_description,
1083 | 'statuses_url': statuses_url,
1084 | },
1085 | 'action': action,
1086 | }
1087 |
1088 |
1089 | commits = [
1090 | {
1091 | 'author': {
1092 | 'login': 'asmeurer',
1093 | },
1094 | 'commit': {
1095 | 'message': "A good commit",
1096 | },
1097 | 'sha': sha,
1098 | 'url': commit_url,
1099 | },
1100 | {
1101 | 'author': {
1102 | 'login': 'certik',
1103 | },
1104 | 'commit': {
1105 | 'message': "A good commit",
1106 | },
1107 | 'sha': sha,
1108 | 'url': commit_url,
1109 | },
1110 | # Test commits without a login
1111 | {
1112 | 'author': None,
1113 | 'commit': {
1114 | 'message': "A good commit",
1115 | },
1116 | 'sha': sha,
1117 | 'url': commit_url,
1118 | },
1119 | ]
1120 |
1121 | # Has comment from sympy-bot
1122 | comments = [
1123 | {
1124 | 'user': {
1125 | 'login': 'sympy-bot',
1126 | },
1127 | 'url': existing_comment_url,
1128 | 'body': release_notes_comment_body,
1129 | },
1130 | {
1131 | 'user': {
1132 | 'login': 'asmeurer',
1133 | },
1134 | 'body': "comment",
1135 | },
1136 | {
1137 | 'user': {
1138 | 'login': 'certik',
1139 | },
1140 | 'body': "comment",
1141 | },
1142 | ]
1143 |
1144 | getiter = {
1145 | commits_url: commits,
1146 | comments_url: comments,
1147 | }
1148 |
1149 | getitem = {}
1150 | post = {
1151 | statuses_url: {},
1152 | comments_url: {
1153 | 'html_url': comment_html_url,
1154 | },
1155 | }
1156 |
1157 | patch = {
1158 | existing_comment_url: {
1159 | 'html_url': comment_html_url,
1160 | 'body': release_notes_comment_body,
1161 | 'url': existing_comment_url,
1162 | },
1163 | }
1164 |
1165 | event = _event(event_data)
1166 |
1167 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
1168 |
1169 | await router.dispatch(event, gh)
1170 |
1171 | getitem_urls = gh.getitem_urls
1172 | getiter_urls = gh.getiter_urls
1173 | post_urls = gh.post_urls
1174 | post_data = gh.post_data
1175 | patch_urls = gh.patch_urls
1176 | patch_data = gh.patch_data
1177 |
1178 | assert set(getiter_urls) == set(getiter), getiter_urls
1179 | assert set(getitem_urls) == set(getitem)
1180 | assert post_urls == [statuses_url, comments_url, statuses_url]
1181 | # Statuses data
1182 | assert len(post_data) == 3
1183 | assert post_data[0] == {
1184 | "state": "failure",
1185 | "target_url": comment_html_url,
1186 | "description": "The release notes check failed",
1187 | "context": "sympy-bot/release-notes",
1188 | }
1189 | assert post_data[1].keys() == {'body'}
1190 | error_message = post_data[1]['body']
1191 | assert ':rotating_light:' in error_message
1192 | assert 'ERROR' in error_message
1193 | assert 'https://github.com/sympy/sympy-bot/issues' in error_message
1194 | assert "The pull request was merged even though the release notes bot had a failing status." in error_message
1195 |
1196 | assert post_data[2] == {
1197 | "state": "error",
1198 | "target_url": comment_html_url,
1199 | "description": "There was an error updating the release notes on the wiki.",
1200 | "context": "sympy-bot/release-notes",
1201 | }
1202 | # Comments data
1203 | assert patch_urls == [existing_comment_url]
1204 | assert len(patch_data) == 1
1205 | assert patch_data[0].keys() == {"body"}
1206 | comment = patch_data[0]["body"]
1207 | assert ":white_check_mark:" not in comment
1208 | assert ":x:" in comment
1209 | assert "new trig solvers" not in comment
1210 | assert "error" not in comment
1211 | assert "There was an issue" in comment
1212 | assert "https://github.com/sympy/sympy-bot" in comment
1213 | for line in invalid_PR_description:
1214 | assert line in comment
1215 | assert "good order" not in comment
1216 | assert "No release notes were found" in comment, comment
1217 |
1218 | assert update_wiki_called_kwargs == {}
1219 |
1220 |
1221 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1222 | async def test_status_bad_new_comment(action):
1223 | event_data = {
1224 | 'pull_request': {
1225 | 'number': 1,
1226 | 'state': 'open',
1227 | 'merged': False,
1228 | 'comments_url': comments_url,
1229 | 'commits_url': commits_url,
1230 | 'head': {
1231 | 'user': {
1232 | 'login': 'asmeurer',
1233 | },
1234 | },
1235 | 'base': {
1236 | 'repo': {
1237 | 'contents_url': contents_url,
1238 | 'html_url': html_url,
1239 | },
1240 | 'ref': 'master',
1241 | },
1242 | 'body': invalid_PR_description,
1243 | 'statuses_url': statuses_url,
1244 | },
1245 | 'action': action,
1246 | }
1247 |
1248 |
1249 | commits = [
1250 | {
1251 | 'author': {
1252 | 'login': 'asmeurer',
1253 | },
1254 | 'commit': {
1255 | 'message': "A good commit",
1256 | },
1257 | 'sha': sha,
1258 | 'url': commit_url,
1259 | },
1260 | {
1261 | 'author': {
1262 | 'login': 'certik',
1263 | },
1264 | 'commit': {
1265 | 'message': "A good commit",
1266 | },
1267 | 'sha': sha,
1268 | 'url': commit_url,
1269 | },
1270 | # Test commits without a login
1271 | {
1272 | 'author': None,
1273 | 'commit': {
1274 | 'message': "A good commit",
1275 | },
1276 | 'sha': sha,
1277 | 'url': commit_url,
1278 | },
1279 | ]
1280 |
1281 | commit = {
1282 | 'files': [
1283 | {
1284 | 'status': 'modified',
1285 | },
1286 | ],
1287 | 'parents': [
1288 | {
1289 | "url": commit_url,
1290 | "sha": sha,
1291 | },
1292 | ],
1293 | }
1294 |
1295 | # No comment from sympy-bot
1296 | comments = [
1297 | {
1298 | 'user': {
1299 | 'login': 'asmeurer',
1300 | },
1301 | 'body': "comment",
1302 | },
1303 | {
1304 | 'user': {
1305 | 'login': 'certik',
1306 | },
1307 | 'body': "comment",
1308 | },
1309 | ]
1310 |
1311 | getiter = {
1312 | commits_url: commits,
1313 | comments_url: comments,
1314 | }
1315 |
1316 | getitem = {
1317 | commit_url: commit,
1318 | }
1319 | post = {
1320 | comments_url: {
1321 | 'html_url': comment_html_url,
1322 | },
1323 | statuses_url: {},
1324 | }
1325 |
1326 | event = _event(event_data)
1327 |
1328 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
1329 |
1330 | await router.dispatch(event, gh)
1331 |
1332 | getitem_urls = gh.getitem_urls
1333 | getiter_urls = gh.getiter_urls
1334 | post_urls = gh.post_urls
1335 | post_data = gh.post_data
1336 | patch_urls = gh.patch_urls
1337 | patch_data = gh.patch_data
1338 |
1339 | assert set(getiter_urls) == set(getiter)
1340 | assert set(getitem_urls) == set(getitem)
1341 | assert post_urls == [comments_url, statuses_url]
1342 | assert len(post_data) == 2
1343 | # Comments data
1344 | assert post_data[0].keys() == {"body"}
1345 | comment = post_data[0]["body"]
1346 | assert ":white_check_mark:" not in comment
1347 | assert ":x:" in comment
1348 | assert "new trig solvers" not in comment
1349 | assert "error" not in comment
1350 | assert "There was an issue" in comment
1351 | assert "https://github.com/sympy/sympy-bot" in comment
1352 | for line in invalid_PR_description:
1353 | assert line in comment
1354 | assert "good order" not in comment
1355 | assert "No release notes were found" in comment
1356 | # Statuses data
1357 | assert post_data[1] == {
1358 | "state": "failure",
1359 | "target_url": comment_html_url,
1360 | "description": "The release notes check failed",
1361 | "context": "sympy-bot/release-notes",
1362 | }
1363 | assert patch_urls == []
1364 | assert patch_data == []
1365 |
1366 |
1367 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1368 | async def test_status_bad_existing_comment(action):
1369 | event_data = {
1370 | 'pull_request': {
1371 | 'number': 1,
1372 | 'state': 'open',
1373 | 'merged': False,
1374 | 'comments_url': comments_url,
1375 | 'commits_url': commits_url,
1376 | 'head': {
1377 | 'user': {
1378 | 'login': 'asmeurer',
1379 | },
1380 | },
1381 | 'base': {
1382 | 'repo': {
1383 | 'contents_url': contents_url,
1384 | 'html_url': html_url,
1385 | },
1386 | 'ref': 'master',
1387 | },
1388 | 'body': invalid_PR_description,
1389 | 'statuses_url': statuses_url,
1390 | },
1391 | 'action': action,
1392 | }
1393 |
1394 |
1395 | commits = [
1396 | {
1397 | 'author': {
1398 | 'login': 'asmeurer',
1399 | },
1400 | 'commit': {
1401 | 'message': "A good commit",
1402 | },
1403 | 'sha': sha,
1404 | 'url': commit_url,
1405 | },
1406 | {
1407 | 'author': {
1408 | 'login': 'certik',
1409 | },
1410 | 'commit': {
1411 | 'message': "A good commit",
1412 | },
1413 | 'sha': sha,
1414 | 'url': commit_url,
1415 | },
1416 | # Test commits without a login
1417 | {
1418 | 'author': None,
1419 | 'commit': {
1420 | 'message': "A good commit",
1421 | },
1422 | 'sha': sha,
1423 | 'url': commit_url,
1424 | },
1425 | ]
1426 |
1427 | commit = {
1428 | 'files': [
1429 | {
1430 | 'status': 'modified',
1431 | },
1432 | ],
1433 | 'parents': [
1434 | {
1435 | "url": commit_url,
1436 | "sha": sha,
1437 | },
1438 | ],
1439 | }
1440 |
1441 | # Has comment from sympy-bot
1442 | comments = [
1443 | {
1444 | 'user': {
1445 | 'login': 'sympy-bot',
1446 | },
1447 | 'url': existing_comment_url,
1448 | 'body': release_notes_comment_body,
1449 | },
1450 | {
1451 | 'user': {
1452 | 'login': 'asmeurer',
1453 | },
1454 | 'body': "comment",
1455 | },
1456 | {
1457 | 'user': {
1458 | 'login': 'certik',
1459 | },
1460 | 'body': "comment",
1461 | },
1462 | ]
1463 |
1464 | getiter = {
1465 | commits_url: commits,
1466 | comments_url: comments,
1467 | }
1468 |
1469 | getitem = {
1470 | commit_url: commit,
1471 | }
1472 | post = {
1473 | statuses_url: {},
1474 | }
1475 |
1476 | patch = {
1477 | existing_comment_url: {
1478 | 'html_url': comment_html_url,
1479 | },
1480 | }
1481 |
1482 | event = _event(event_data)
1483 |
1484 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
1485 |
1486 | await router.dispatch(event, gh)
1487 |
1488 | getitem_urls = gh.getitem_urls
1489 | getiter_urls = gh.getiter_urls
1490 | post_urls = gh.post_urls
1491 | post_data = gh.post_data
1492 | patch_urls = gh.patch_urls
1493 | patch_data = gh.patch_data
1494 |
1495 | assert set(getiter_urls) == set(getiter)
1496 | assert set(getitem_urls) == set(getitem)
1497 | assert post_urls == [statuses_url]
1498 | # Statuses data
1499 | assert post_data == [{
1500 | "state": "failure",
1501 | "target_url": comment_html_url,
1502 | "description": "The release notes check failed",
1503 | "context": "sympy-bot/release-notes",
1504 | }]
1505 | # Comments data
1506 | assert patch_urls == [existing_comment_url]
1507 | assert len(patch_data) == 1
1508 | assert patch_data[0].keys() == {"body"}
1509 | comment = patch_data[0]["body"]
1510 | assert ":white_check_mark:" not in comment
1511 | assert ":x:" in comment
1512 | assert "new trig solvers" not in comment
1513 | assert "error" not in comment
1514 | assert "There was an issue" in comment
1515 | assert "https://github.com/sympy/sympy-bot" in comment
1516 | for line in invalid_PR_description:
1517 | assert line in comment
1518 | assert "good order" not in comment
1519 | assert "No release notes were found" in comment, comment
1520 |
1521 |
1522 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1523 | async def test_rate_limit_comment(action):
1524 | # Based on test_status_good_new_comment
1525 | event_data = {
1526 | 'pull_request': {
1527 | 'number': 1,
1528 | 'state': 'open',
1529 | 'merged': False,
1530 | 'comments_url': comments_url,
1531 | 'commits_url': commits_url,
1532 | 'head': {
1533 | 'user': {
1534 | 'login': 'asmeurer',
1535 | },
1536 | },
1537 | 'base': {
1538 | 'repo': {
1539 | 'contents_url': contents_url,
1540 | 'html_url': html_url,
1541 | },
1542 | 'ref': 'master',
1543 | },
1544 | 'body': valid_PR_description,
1545 | 'statuses_url': statuses_url,
1546 | },
1547 | 'action': action,
1548 | }
1549 |
1550 |
1551 | commits = [
1552 | {
1553 | 'author': {
1554 | 'login': 'asmeurer',
1555 | },
1556 | 'commit': {
1557 | 'message': "A good commit",
1558 | },
1559 | 'sha': sha,
1560 | 'url': commit_url,
1561 | },
1562 | {
1563 | 'author': {
1564 | 'login': 'certik',
1565 | },
1566 | 'commit': {
1567 | 'message': "A good commit",
1568 | },
1569 | 'sha': sha,
1570 | 'url': commit_url,
1571 | },
1572 | # Test commits without a login
1573 | {
1574 | 'author': None,
1575 | 'commit': {
1576 | 'message': "A good commit",
1577 | },
1578 | 'sha': sha,
1579 | 'url': commit_url,
1580 | },
1581 | ]
1582 |
1583 | commit = {
1584 | 'files': [
1585 | {
1586 | 'status': 'modified',
1587 | },
1588 | ],
1589 | 'parents': [
1590 | {
1591 | "url": commit_url,
1592 | "sha": sha,
1593 | },
1594 | ],
1595 | }
1596 |
1597 | # No comment from sympy-bot
1598 | comments = [
1599 | {
1600 | 'user': {
1601 | 'login': 'asmeurer',
1602 | },
1603 | 'body': "comment",
1604 | },
1605 | {
1606 | 'user': {
1607 | 'login': 'certik',
1608 | },
1609 | "body": "comment",
1610 | },
1611 | ]
1612 |
1613 | version_file = {
1614 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
1615 | }
1616 |
1617 | getiter = {
1618 | commits_url: commits,
1619 | comments_url: comments,
1620 | }
1621 |
1622 | getitem = {
1623 | version_url: version_file,
1624 | commit_url: commit,
1625 | }
1626 | post = {
1627 | comments_url: {
1628 | 'html_url': comment_html_url,
1629 | },
1630 | statuses_url: {},
1631 | }
1632 |
1633 | event = _event(event_data)
1634 |
1635 | now = datetime.datetime.now(datetime.timezone.utc)
1636 | reset_datetime = now + datetime.timedelta(hours=1)
1637 | rate_limit = FakeRateLimit(remaining=5, limit=1000, reset_datetime=reset_datetime)
1638 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, rate_limit=rate_limit)
1639 |
1640 | await router.dispatch(event, gh)
1641 |
1642 | # Everything else is already tested in test_status_good_new_comment()
1643 | # above
1644 | post_urls = gh.post_urls
1645 | post_data = gh.post_data
1646 | assert post_urls == [comments_url, statuses_url, comments_url]
1647 | assert len(post_data) == 3
1648 | assert post_data[2].keys() == {"body"}
1649 | comment = post_data[2]["body"]
1650 | assert ":warning:" in comment
1651 | assert "5" in comment
1652 | assert "1000" in comment
1653 | assert str(reset_datetime) in comment
1654 |
1655 |
1656 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1657 | async def test_header_in_message(action):
1658 | # Based on test_status_good_new_comment
1659 | event_data = {
1660 | 'pull_request': {
1661 | 'number': 1,
1662 | 'state': 'open',
1663 | 'merged': False,
1664 | 'comments_url': comments_url,
1665 | 'commits_url': commits_url,
1666 | 'head': {
1667 | 'user': {
1668 | 'login': 'asmeurer',
1669 | },
1670 | },
1671 | 'base': {
1672 | 'repo': {
1673 | 'contents_url': contents_url,
1674 | 'html_url': html_url,
1675 | },
1676 | 'ref': 'master',
1677 | },
1678 | 'body': valid_PR_description,
1679 | 'statuses_url': statuses_url,
1680 | },
1681 | 'action': action,
1682 | }
1683 |
1684 | sha_1 = '174b8b37bc33e9eb29e710a233190d02a13bdb54'
1685 | commits = [
1686 | {
1687 | 'author': {
1688 | 'login': 'asmeurer',
1689 | },
1690 | 'commit': {
1691 | 'message': """
1692 |
1693 | * solvers
1694 | * solver change
1695 |
1696 | """
1697 | },
1698 | 'sha': sha_1,
1699 | 'url': commit_url_template.format(sha=sha_1)
1700 | },
1701 | {
1702 | 'author': {
1703 | 'login': 'certik',
1704 | },
1705 | 'commit': {
1706 | 'message': "A good commit",
1707 | },
1708 | 'sha': sha,
1709 | 'url': commit_url,
1710 | },
1711 | # Test commits without a login
1712 | {
1713 | 'author': None,
1714 | 'commit': {
1715 | 'message': "A good commit",
1716 | },
1717 | 'sha': sha,
1718 | 'url': commit_url,
1719 | },
1720 | ]
1721 |
1722 | commit = {
1723 | 'files': [
1724 | {
1725 | 'status': 'modified',
1726 | },
1727 | ],
1728 | 'parents': [
1729 | {
1730 | "url": commit_url,
1731 | "sha": sha,
1732 | },
1733 | ],
1734 | }
1735 |
1736 | # No comment from sympy-bot
1737 | comments = [
1738 | {
1739 | 'user': {
1740 | 'login': 'asmeurer',
1741 | },
1742 | 'body': "comment",
1743 | },
1744 | {
1745 | 'user': {
1746 | 'login': 'certik',
1747 | },
1748 | 'body': "comment",
1749 | },
1750 | ]
1751 |
1752 | getiter = {
1753 | commits_url: commits,
1754 | comments_url: comments,
1755 | }
1756 |
1757 | getitem = {
1758 | commit_url: commit,
1759 | commit_url_template.format(sha=sha_1): commit,
1760 | }
1761 | post = {
1762 | comments_url: {
1763 | 'html_url': comment_html_url,
1764 | },
1765 | statuses_url: {},
1766 | }
1767 |
1768 | event = _event(event_data)
1769 |
1770 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
1771 |
1772 | await router.dispatch(event, gh)
1773 |
1774 | getitem_urls = gh.getitem_urls
1775 | getiter_urls = gh.getiter_urls
1776 | post_urls = gh.post_urls
1777 | post_data = gh.post_data
1778 | patch_urls = gh.patch_urls
1779 | patch_data = gh.patch_data
1780 |
1781 | # The rest is already tested in test_status_good_new_comment
1782 | assert set(getiter_urls) == set(getiter)
1783 | assert set(getitem_urls) == set(getitem)
1784 | assert post_urls == [comments_url, statuses_url]
1785 | assert len(post_data) == 2
1786 | # Comments data
1787 | assert post_data[0].keys() == {"body"}
1788 | comment = post_data[0]["body"]
1789 | assert ":white_check_mark:" not in comment
1790 | assert ":x:" in comment
1791 | assert "error" not in comment
1792 | assert "https://github.com/sympy/sympy-bot" in comment
1793 | assert "good order" not in comment
1794 | assert sha_1 in comment
1795 | assert "" in comment
1796 | assert "" in comment
1797 | # Statuses data
1798 | assert post_data[1] == {
1799 | "state": "failure",
1800 | "target_url": comment_html_url,
1801 | "description": "The release notes check failed",
1802 | "context": "sympy-bot/release-notes",
1803 | }
1804 | assert patch_urls == []
1805 | assert patch_data == []
1806 |
1807 |
1808 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1809 | async def test_bad_version_file(action):
1810 | event_data = {
1811 | 'pull_request': {
1812 | 'number': 1,
1813 | 'state': 'open',
1814 | 'merged': False,
1815 | 'comments_url': comments_url,
1816 | 'commits_url': commits_url,
1817 | 'head': {
1818 | 'user': {
1819 | 'login': 'asmeurer',
1820 | },
1821 | },
1822 | 'base': {
1823 | 'repo': {
1824 | 'contents_url': contents_url,
1825 | 'html_url': html_url,
1826 | },
1827 | 'ref': 'master',
1828 | },
1829 | 'body': valid_PR_description,
1830 | 'statuses_url': statuses_url,
1831 | },
1832 | 'action': action,
1833 | }
1834 |
1835 |
1836 | commits = [
1837 | {
1838 | 'author': {
1839 | 'login': 'asmeurer',
1840 | },
1841 | 'commit': {
1842 | 'message': "A good commit",
1843 | },
1844 | 'sha': sha,
1845 | 'url': commit_url,
1846 | },
1847 | {
1848 | 'author': {
1849 | 'login': 'certik',
1850 | },
1851 | 'commit': {
1852 | 'message': "A good commit",
1853 | },
1854 | 'sha': sha,
1855 | 'url': commit_url,
1856 | },
1857 | # Test commits without a login
1858 | {
1859 | 'author': None,
1860 | 'commit': {
1861 | 'message': "A good commit",
1862 | },
1863 | 'sha': sha,
1864 | 'url': commit_url,
1865 | },
1866 | ]
1867 |
1868 | commit = {
1869 | 'files': [
1870 | {
1871 | 'status': 'modified',
1872 | },
1873 | ],
1874 | 'parents': [
1875 | {
1876 | "url": commit_url,
1877 | "sha": sha,
1878 | },
1879 | ],
1880 | }
1881 |
1882 | # No comment from sympy-bot
1883 | comments = [
1884 | {
1885 | 'user': {
1886 | 'login': 'asmeurer',
1887 | },
1888 | },
1889 | {
1890 | 'user': {
1891 | 'login': 'certik',
1892 | },
1893 | },
1894 | ]
1895 |
1896 | version_file = {
1897 | 'content': base64.b64encode(b'\n'),
1898 | }
1899 |
1900 | getiter = {
1901 | commits_url: commits,
1902 | comments_url: comments,
1903 | }
1904 |
1905 | getitem = {
1906 | version_url: version_file,
1907 | commit_url: commit,
1908 | }
1909 | post = {
1910 | comments_url: {
1911 | 'html_url': comment_html_url,
1912 | },
1913 | statuses_url: {},
1914 | }
1915 |
1916 | event = _event(event_data)
1917 |
1918 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
1919 |
1920 | await router.dispatch(event, gh)
1921 |
1922 | getitem_urls = gh.getitem_urls
1923 | getiter_urls = gh.getiter_urls
1924 | post_urls = gh.post_urls
1925 | post_data = gh.post_data
1926 | patch_urls = gh.patch_urls
1927 | patch_data = gh.patch_data
1928 |
1929 | assert set(getiter_urls) == set(getiter)
1930 | assert set(getitem_urls) == set(getitem)
1931 | assert post_urls == [comments_url, statuses_url]
1932 | assert len(post_data) == 2
1933 | # Comments data
1934 | assert post_data[0].keys() == {"body"}
1935 | comment = post_data[0]["body"]
1936 | assert ":white_check_mark:" not in comment
1937 | assert ":x:" in comment
1938 | assert "error" in comment
1939 | assert "https://github.com/sympy/sympy-bot" in comment
1940 | assert "sympy/release.py" in comment
1941 | assert "There was an error getting the version" in comment
1942 | assert "https://github.com/sympy/sympy-bot/issues" in comment
1943 | for line in valid_PR_description:
1944 | assert line in comment
1945 | assert "good order" not in comment
1946 | # Statuses data
1947 | assert post_data[1] == {
1948 | "state": "error",
1949 | "target_url": comment_html_url,
1950 | "description": "The release notes check failed",
1951 | "context": "sympy-bot/release-notes",
1952 | }
1953 | assert patch_urls == []
1954 | assert patch_data == []
1955 |
1956 |
1957 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
1958 | @parametrize('include_extra', [True, False])
1959 | async def test_no_user_logins_in_commits(action, include_extra):
1960 | event_data = {
1961 | 'pull_request': {
1962 | 'number': 1,
1963 | 'state': 'open',
1964 | 'merged': False,
1965 | 'comments_url': comments_url,
1966 | 'commits_url': commits_url,
1967 | 'head': {
1968 | 'user': {
1969 | 'login': 'asmeurer',
1970 | },
1971 | },
1972 | 'base': {
1973 | 'repo': {
1974 | 'contents_url': contents_url,
1975 | 'html_url': html_url,
1976 | },
1977 | 'ref': 'master',
1978 | },
1979 | 'body': valid_PR_description,
1980 | 'statuses_url': statuses_url,
1981 | },
1982 | 'action': action,
1983 | }
1984 |
1985 |
1986 | commits = [
1987 | {
1988 | 'author': None,
1989 | 'commit': {
1990 | 'message': "A good commit",
1991 | },
1992 | 'sha': sha,
1993 | 'url': commit_url,
1994 | },
1995 | ]
1996 |
1997 | commit = {
1998 | 'files': [
1999 | {
2000 | 'status': 'modified',
2001 | },
2002 | ],
2003 | 'parents': [
2004 | {
2005 | "url": commit_url,
2006 | "sha": sha,
2007 | },
2008 | ],
2009 | }
2010 |
2011 | if include_extra:
2012 | commits += [
2013 | {
2014 | 'author': {
2015 | 'login': 'certik',
2016 | },
2017 | 'commit': {
2018 | 'message': "A good commit",
2019 | },
2020 | 'sha': sha,
2021 | 'url': commit_url,
2022 | },
2023 | ]
2024 |
2025 | # No comment from sympy-bot
2026 | comments = [
2027 | {
2028 | 'user': {
2029 | 'login': 'asmeurer',
2030 | },
2031 | 'body': "comment",
2032 | },
2033 | {
2034 | 'user': {
2035 | 'login': 'certik',
2036 | },
2037 | 'body': "comment",
2038 | },
2039 | ]
2040 |
2041 | version_file = {
2042 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
2043 | }
2044 |
2045 | getiter = {
2046 | commits_url: commits,
2047 | comments_url: comments,
2048 | }
2049 |
2050 | getitem = {
2051 | version_url: version_file,
2052 | commit_url: commit,
2053 | }
2054 | post = {
2055 | comments_url: {
2056 | 'html_url': comment_html_url,
2057 | },
2058 | statuses_url: {},
2059 | }
2060 |
2061 | event = _event(event_data)
2062 |
2063 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
2064 |
2065 | await router.dispatch(event, gh)
2066 |
2067 | getitem_urls = gh.getitem_urls
2068 | getiter_urls = gh.getiter_urls
2069 | post_urls = gh.post_urls
2070 | post_data = gh.post_data
2071 | patch_urls = gh.patch_urls
2072 | patch_data = gh.patch_data
2073 |
2074 | assert set(getiter_urls) == set(getiter)
2075 | assert set(getitem_urls) == set(getitem)
2076 | assert post_urls == [comments_url, statuses_url]
2077 | assert len(post_data) == 2
2078 | # Comments data
2079 | assert post_data[0].keys() == {"body"}
2080 | comment = post_data[0]["body"]
2081 | assert ":white_check_mark:" in comment
2082 | assert ":x:" not in comment
2083 | assert "new trig solvers" in comment
2084 | assert "error" not in comment
2085 | assert "https://github.com/sympy/sympy-bot" in comment
2086 | for line in valid_PR_description:
2087 | assert line in comment
2088 | assert "good order" in comment
2089 | assert "@asmeurer" in comment
2090 | if include_extra:
2091 | assert "@certik" in comment
2092 | # Statuses data
2093 | assert post_data[1] == {
2094 | "state": "success",
2095 | "target_url": comment_html_url,
2096 | "description": "The release notes look OK",
2097 | "context": "sympy-bot/release-notes",
2098 | }
2099 | assert patch_urls == []
2100 | assert patch_data == []
2101 |
2102 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
2103 | async def test_status_good_new_comment_other_base(action):
2104 | # Based on test_status_good_new_comment
2105 | event_data = {
2106 | 'pull_request': {
2107 | 'number': 1,
2108 | 'state': 'open',
2109 | 'merged': False,
2110 | 'comments_url': comments_url,
2111 | 'commits_url': commits_url,
2112 | 'head': {
2113 | 'user': {
2114 | 'login': 'asmeurer',
2115 | },
2116 | },
2117 | 'base': {
2118 | 'repo': {
2119 | 'contents_url': contents_url,
2120 | 'html_url': html_url,
2121 | },
2122 | 'ref': '1.4',
2123 | },
2124 | 'body': valid_PR_description,
2125 | 'statuses_url': statuses_url,
2126 | },
2127 | 'action': action,
2128 | }
2129 |
2130 |
2131 | commits = [
2132 | {
2133 | 'author': {
2134 | 'login': 'asmeurer',
2135 | },
2136 | 'commit': {
2137 | 'message': "A good commit",
2138 | },
2139 | 'sha': sha,
2140 | 'url': commit_url,
2141 | },
2142 | {
2143 | 'author': {
2144 | 'login': 'certik',
2145 | },
2146 | 'commit': {
2147 | 'message': "A good commit",
2148 | },
2149 | 'sha': sha,
2150 | 'url': commit_url,
2151 | },
2152 | # Test commits without a login
2153 | {
2154 | 'author': None,
2155 | 'commit': {
2156 | 'message': "A good commit",
2157 | },
2158 | 'sha': sha,
2159 | 'url': commit_url,
2160 | },
2161 | ]
2162 |
2163 | commit = {
2164 | 'files': [
2165 | {
2166 | 'status': 'modified',
2167 | },
2168 | ],
2169 | 'parents': [
2170 | {
2171 | "url": commit_url,
2172 | "sha": sha,
2173 | },
2174 | ],
2175 | }
2176 |
2177 | # No comment from sympy-bot
2178 | comments = [
2179 | {
2180 | 'user': {
2181 | 'login': 'asmeurer',
2182 | },
2183 | },
2184 | {
2185 | 'user': {
2186 | 'login': 'certik',
2187 | },
2188 | },
2189 | ]
2190 |
2191 | version_file = {
2192 | 'content': base64.b64encode(b'__version__ = "1.4rc1"\n'),
2193 | }
2194 |
2195 | getiter = {
2196 | commits_url: commits,
2197 | comments_url: comments,
2198 | }
2199 |
2200 | getitem = {
2201 | version_url_template.format(ref='1.4'): version_file,
2202 | commit_url: commit,
2203 | }
2204 | post = {
2205 | comments_url: {
2206 | 'html_url': comment_html_url,
2207 | },
2208 | statuses_url: {},
2209 | }
2210 |
2211 | event = _event(event_data)
2212 |
2213 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post)
2214 |
2215 | await router.dispatch(event, gh)
2216 |
2217 | getitem_urls = gh.getitem_urls
2218 | getiter_urls = gh.getiter_urls
2219 | post_urls = gh.post_urls
2220 | post_data = gh.post_data
2221 | patch_urls = gh.patch_urls
2222 | patch_data = gh.patch_data
2223 |
2224 | assert set(getiter_urls) == set(getiter)
2225 | assert set(getitem_urls) == set(getitem)
2226 | assert post_urls == [comments_url, statuses_url]
2227 | assert len(post_data) == 2
2228 | # Comments data
2229 | assert post_data[0].keys() == {"body"}
2230 | comment = post_data[0]["body"]
2231 | assert ":white_check_mark:" in comment
2232 | assert ":x:" not in comment
2233 | assert "new trig solvers" in comment
2234 | assert "error" not in comment
2235 | assert "https://github.com/sympy/sympy-bot" in comment
2236 | assert '1.2.1' not in comment
2237 | assert '1.4' in comment
2238 | for line in valid_PR_description:
2239 | assert line in comment
2240 | assert "good order" in comment
2241 | # Statuses data
2242 | assert post_data[1] == {
2243 | "state": "success",
2244 | "target_url": comment_html_url,
2245 | "description": "The release notes look OK",
2246 | "context": "sympy-bot/release-notes",
2247 | }
2248 | assert patch_urls == []
2249 | assert patch_data == []
2250 |
2251 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
2252 | async def test_added_deleted_new_comment(action):
2253 | # Based on test_status_good_existing_comment
2254 | event_data = {
2255 | 'pull_request': {
2256 | 'number': 1,
2257 | 'state': 'open',
2258 | 'merged': False,
2259 | 'comments_url': comments_url,
2260 | 'commits_url': commits_url,
2261 | 'head': {
2262 | 'user': {
2263 | 'login': 'asmeurer',
2264 | },
2265 | },
2266 | 'base': {
2267 | 'repo': {
2268 | 'contents_url': contents_url,
2269 | 'html_url': html_url,
2270 | },
2271 | 'ref': 'master',
2272 | },
2273 | 'body': valid_PR_description,
2274 | 'statuses_url': statuses_url,
2275 | },
2276 | 'action': action,
2277 | }
2278 |
2279 | sha_merge = '61697bd7249381b27a4b5d449a8061086effd381'
2280 | sha_1 = '174b8b37bc33e9eb29e710a233190d02a13bdb54'
2281 | sha_2 = 'aef484a1d46bb5389f1709d78e39126d9cb8599f'
2282 | sha_3 = sha
2283 |
2284 | commits = [
2285 | {
2286 | 'author': {
2287 | 'login': 'asmeurer',
2288 | },
2289 | 'commit': {
2290 | 'message': "Merge"
2291 | },
2292 | 'sha': sha_merge,
2293 | 'url': commit_url_template.format(sha=sha_merge)
2294 | },
2295 | {
2296 | 'author': {
2297 | 'login': 'asmeurer',
2298 | },
2299 | 'commit': {
2300 | 'message': "Adds file1"
2301 | },
2302 | 'sha': sha_1,
2303 | 'url': commit_url_template.format(sha=sha_1)
2304 | },
2305 | {
2306 | 'author': {
2307 | 'login': 'asmeurer',
2308 | },
2309 | 'commit': {
2310 | 'message': "Modifies file1",
2311 | },
2312 | 'sha': sha_2,
2313 | 'url': commit_url_template.format(sha=sha_2),
2314 | },
2315 | {
2316 | 'author': {
2317 | 'login': 'asmeurer',
2318 | },
2319 | 'commit': {
2320 | 'message': "Deletes file1",
2321 | },
2322 | 'sha': sha_3,
2323 | 'url': commit_url_template.format(sha=sha_3),
2324 | },
2325 | ]
2326 |
2327 | commit_merge = {
2328 | 'sha': sha_1,
2329 | 'files': [
2330 | {
2331 | 'filename': 'file1',
2332 | 'status': 'added',
2333 | },
2334 | {
2335 | 'filename': 'file2',
2336 | 'status': 'deleted',
2337 | },
2338 | ],
2339 | 'parents': [
2340 | {
2341 | "url": commit_url,
2342 | "sha": sha_2,
2343 | },
2344 | {
2345 | "url": commit_url,
2346 | "sha": sha,
2347 | },
2348 | ],
2349 | }
2350 |
2351 | commit_add = {
2352 | 'sha': sha_1,
2353 | 'files': [
2354 | {
2355 | 'filename': 'file1',
2356 | 'status': 'added',
2357 | },
2358 | ],
2359 | 'parents': [
2360 | {
2361 | "url": commit_url,
2362 | "sha": sha,
2363 | },
2364 | ],
2365 | }
2366 |
2367 | commit_modify = {
2368 | 'sha': sha_2,
2369 | 'files': [
2370 | {
2371 | 'filename': 'file1',
2372 | 'status': 'modified',
2373 | },
2374 | ],
2375 | 'parents': [
2376 | {
2377 | "url": commit_url,
2378 | "sha": sha,
2379 | },
2380 | ],
2381 | }
2382 |
2383 | commit_delete = {
2384 | 'sha': sha_3,
2385 | 'files': [
2386 | {
2387 | 'filename': 'file1',
2388 | 'status': 'removed',
2389 | },
2390 | ],
2391 | 'parents': [
2392 | {
2393 | "url": commit_url,
2394 | "sha": sha,
2395 | },
2396 | ],
2397 | }
2398 |
2399 | comments = [
2400 | {
2401 | 'user': {
2402 | 'login': 'sympy-bot',
2403 | },
2404 | 'url': existing_comment_url,
2405 | 'body': release_notes_comment_body,
2406 | },
2407 | {
2408 | 'user': {
2409 | 'login': 'asmeurer',
2410 | },
2411 | 'body': "comment",
2412 | },
2413 | {
2414 | 'user': {
2415 | 'login': 'certik',
2416 | },
2417 | 'body': "comment",
2418 | },
2419 | ]
2420 |
2421 | version_file = {
2422 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
2423 | }
2424 |
2425 | getiter = {
2426 | commits_url: commits,
2427 | comments_url: comments,
2428 | }
2429 |
2430 | getitem = {
2431 | commit_url_template.format(sha=sha_merge): commit_merge,
2432 | commit_url_template.format(sha=sha_1): commit_add,
2433 | commit_url_template.format(sha=sha_2): commit_modify,
2434 | commit_url_template.format(sha=sha_3): commit_delete,
2435 | version_url: version_file,
2436 | }
2437 | post = {
2438 | comments_url: {
2439 | 'html_url': comment_html_url,
2440 | },
2441 | statuses_url: {},
2442 | }
2443 | patch = {
2444 | existing_comment_url: {
2445 | 'html_url': comment_html_url,
2446 | },
2447 | }
2448 |
2449 | event = _event(event_data)
2450 |
2451 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
2452 |
2453 | await router.dispatch(event, gh)
2454 |
2455 | getitem_urls = gh.getitem_urls
2456 | getiter_urls = gh.getiter_urls
2457 | post_urls = gh.post_urls
2458 | post_data = gh.post_data
2459 | patch_urls = gh.patch_urls
2460 |
2461 | # The rest is already tested in test_status_good_new_comment
2462 | assert set(getiter_urls) == set(getiter)
2463 | assert set(getitem_urls) == set(getitem)
2464 | assert post_urls == [statuses_url, comments_url]
2465 | assert patch_urls == [existing_comment_url]
2466 | assert len(post_data) == 2
2467 | # Comments data
2468 | assert post_data[1].keys() == {"body"}
2469 | comment = post_data[1]["body"]
2470 | assert ":white_check_mark:" not in comment
2471 | assert ":x:" not in comment
2472 | assert "\U0001f7e0" in comment
2473 | assert "error" not in comment
2474 | assert "add new files" in comment
2475 | assert "delete files" in comment
2476 | assert "https://github.com/sympy/sympy-bot" in comment
2477 | assert sha_1 in comment
2478 | assert sha_2 not in comment
2479 | assert sha_3 in comment
2480 | assert sha_merge not in comment
2481 | assert "`file1`" in comment
2482 | assert "" not in comment
2483 | assert "" not in comment
2484 |
2485 |
2486 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
2487 | async def test_added_deleted_existing_comment(action):
2488 | # Based on test_status_good_existing_comment
2489 | event_data = {
2490 | 'pull_request': {
2491 | 'number': 1,
2492 | 'state': 'open',
2493 | 'merged': False,
2494 | 'comments_url': comments_url,
2495 | 'commits_url': commits_url,
2496 | 'head': {
2497 | 'user': {
2498 | 'login': 'asmeurer',
2499 | },
2500 | },
2501 | 'base': {
2502 | 'repo': {
2503 | 'contents_url': contents_url,
2504 | 'html_url': html_url,
2505 | },
2506 | 'ref': 'master',
2507 | },
2508 | 'body': valid_PR_description,
2509 | 'statuses_url': statuses_url,
2510 | },
2511 | 'action': action,
2512 | }
2513 |
2514 | sha_merge = '61697bd7249381b27a4b5d449a8061086effd381'
2515 | sha_1 = '174b8b37bc33e9eb29e710a233190d02a13bdb54'
2516 | sha_2 = 'aef484a1d46bb5389f1709d78e39126d9cb8599f'
2517 | sha_3 = sha
2518 |
2519 | commits = [
2520 | {
2521 | 'author': {
2522 | 'login': 'asmeurer',
2523 | },
2524 | 'commit': {
2525 | 'message': "Merge"
2526 | },
2527 | 'sha': sha_merge,
2528 | 'url': commit_url_template.format(sha=sha_merge)
2529 | },
2530 | {
2531 | 'author': {
2532 | 'login': 'asmeurer',
2533 | },
2534 | 'commit': {
2535 | 'message': "Adds file1"
2536 | },
2537 | 'sha': sha_1,
2538 | 'url': commit_url_template.format(sha=sha_1)
2539 | },
2540 | {
2541 | 'author': {
2542 | 'login': 'asmeurer',
2543 | },
2544 | 'commit': {
2545 | 'message': "Modifies file1",
2546 | },
2547 | 'sha': sha_2,
2548 | 'url': commit_url_template.format(sha=sha_2),
2549 | },
2550 | {
2551 | 'author': {
2552 | 'login': 'asmeurer',
2553 | },
2554 | 'commit': {
2555 | 'message': "Deletes file1",
2556 | },
2557 | 'sha': sha_3,
2558 | 'url': commit_url_template.format(sha=sha_3),
2559 | },
2560 | ]
2561 |
2562 | commit_merge = {
2563 | 'sha': sha_1,
2564 | 'files': [
2565 | {
2566 | 'filename': 'file1',
2567 | 'status': 'added',
2568 | },
2569 | {
2570 | 'filename': 'file2',
2571 | 'status': 'deleted',
2572 | },
2573 | ],
2574 | 'parents': [
2575 | {
2576 | "url": commit_url,
2577 | "sha": sha_2,
2578 | },
2579 | {
2580 | "url": commit_url,
2581 | "sha": sha,
2582 | },
2583 | ],
2584 | }
2585 |
2586 | commit_add = {
2587 | 'sha': sha_1,
2588 | 'files': [
2589 | {
2590 | 'filename': 'file1',
2591 | 'status': 'added',
2592 | },
2593 | ],
2594 | 'parents': [
2595 | {
2596 | "url": commit_url,
2597 | "sha": sha,
2598 | },
2599 | ],
2600 | }
2601 |
2602 | commit_modify = {
2603 | 'sha': sha_2,
2604 | 'files': [
2605 | {
2606 | 'filename': 'file1',
2607 | 'status': 'modified',
2608 | },
2609 | ],
2610 | 'parents': [
2611 | {
2612 | "url": commit_url,
2613 | "sha": sha,
2614 | },
2615 | ],
2616 | }
2617 |
2618 | commit_delete = {
2619 | 'sha': sha_3,
2620 | 'files': [
2621 | {
2622 | 'filename': 'file1',
2623 | 'status': 'removed',
2624 | },
2625 | ],
2626 | 'parents': [
2627 | {
2628 | "url": commit_url,
2629 | "sha": sha,
2630 | },
2631 | ],
2632 | }
2633 |
2634 | comments = [
2635 | {
2636 | 'user': {
2637 | 'login': 'sympy-bot',
2638 | },
2639 | 'url': existing_comment_url,
2640 | 'body': release_notes_comment_body,
2641 | },
2642 | {
2643 | 'user': {
2644 | 'login': 'sympy-bot',
2645 | },
2646 | 'url': existing_added_deleted_comment_url,
2647 | 'body': added_deleted_comment_body,
2648 | },
2649 | {
2650 | 'user': {
2651 | 'login': 'asmeurer',
2652 | },
2653 | 'body': "comment",
2654 | },
2655 | {
2656 | 'user': {
2657 | 'login': 'certik',
2658 | },
2659 | 'body': "comment",
2660 | },
2661 | ]
2662 |
2663 | version_file = {
2664 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
2665 | }
2666 |
2667 | getiter = {
2668 | commits_url: commits,
2669 | comments_url: comments,
2670 | }
2671 |
2672 | getitem = {
2673 | commit_url_template.format(sha=sha_merge): commit_merge,
2674 | commit_url_template.format(sha=sha_1): commit_add,
2675 | commit_url_template.format(sha=sha_2): commit_modify,
2676 | commit_url_template.format(sha=sha_3): commit_delete,
2677 | version_url: version_file,
2678 | }
2679 | post = {
2680 | comments_url: {
2681 | 'html_url': comment_html_url,
2682 | },
2683 | statuses_url: {},
2684 | }
2685 | patch = {
2686 | existing_comment_url: {
2687 | 'html_url': comment_html_url,
2688 | },
2689 | existing_added_deleted_comment_url: {
2690 | 'html_url': comment_html_url2,
2691 | },
2692 | }
2693 |
2694 | event = _event(event_data)
2695 |
2696 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch)
2697 |
2698 | await router.dispatch(event, gh)
2699 |
2700 | getitem_urls = gh.getitem_urls
2701 | getiter_urls = gh.getiter_urls
2702 | post_urls = gh.post_urls
2703 | post_data = gh.post_data
2704 | patch_urls = gh.patch_urls
2705 | patch_data = gh.patch_data
2706 |
2707 | assert set(getiter_urls) == set(getiter)
2708 | assert set(getitem_urls) == set(getitem)
2709 | assert post_urls == [statuses_url]
2710 | assert patch_urls == list(patch)
2711 | assert len(post_data) == 1
2712 | assert len(patch_data) == 2
2713 | # Comments data.
2714 | assert patch_data[1].keys() == {"body"}
2715 | comment = patch_data[1]["body"]
2716 | assert ":white_check_mark:" not in comment
2717 | assert ":x:" not in comment
2718 | assert "\U0001f7e0" in comment
2719 | assert "error" not in comment
2720 | assert "add new files" in comment
2721 | assert "delete files" in comment
2722 | assert "https://github.com/sympy/sympy-bot" in comment
2723 | assert sha_1 in comment
2724 | assert sha_2 not in comment
2725 | assert sha_3 in comment
2726 | assert sha_merge not in comment
2727 | assert "`file1`" in comment
2728 | assert "" not in comment
2729 | assert "" not in comment
2730 |
2731 | @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited'])
2732 | async def test_added_deleted_remove_existing_comment(action):
2733 | # Based on test_status_good_existing_comment
2734 | event_data = {
2735 | 'pull_request': {
2736 | 'number': 1,
2737 | 'state': 'open',
2738 | 'merged': False,
2739 | 'comments_url': comments_url,
2740 | 'commits_url': commits_url,
2741 | 'head': {
2742 | 'user': {
2743 | 'login': 'asmeurer',
2744 | },
2745 | },
2746 | 'base': {
2747 | 'repo': {
2748 | 'contents_url': contents_url,
2749 | 'html_url': html_url,
2750 | },
2751 | 'ref': 'master',
2752 | },
2753 | 'body': valid_PR_description,
2754 | 'statuses_url': statuses_url,
2755 | },
2756 | 'action': action,
2757 | }
2758 |
2759 | commits = [
2760 | {
2761 | 'author': {
2762 | 'login': 'asmeurer',
2763 | },
2764 | 'commit': {
2765 | 'message': "Modifies file1"
2766 | },
2767 | 'sha': sha,
2768 | 'url': commit_url,
2769 | },
2770 | ]
2771 |
2772 | commit = {
2773 | 'sha': sha,
2774 | 'files': [
2775 | {
2776 | 'filename': 'file1',
2777 | 'status': 'modified',
2778 | },
2779 | ],
2780 | 'parents': [
2781 | {
2782 | "url": commit_url,
2783 | "sha": sha,
2784 | },
2785 | ],
2786 | }
2787 |
2788 | comments = [
2789 | {
2790 | 'user': {
2791 | 'login': 'sympy-bot',
2792 | },
2793 | 'url': existing_comment_url,
2794 | 'body': release_notes_comment_body,
2795 | },
2796 | {
2797 | 'user': {
2798 | 'login': 'sympy-bot',
2799 | },
2800 | 'url': existing_added_deleted_comment_url,
2801 | 'body': added_deleted_comment_body,
2802 | },
2803 | {
2804 | 'user': {
2805 | 'login': 'asmeurer',
2806 | },
2807 | 'body': "comment",
2808 | },
2809 | {
2810 | 'user': {
2811 | 'login': 'certik',
2812 | },
2813 | 'body': "comment",
2814 | },
2815 | ]
2816 |
2817 | version_file = {
2818 | 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'),
2819 | }
2820 |
2821 | getiter = {
2822 | commits_url: commits,
2823 | comments_url: comments,
2824 | }
2825 |
2826 | getitem = {
2827 | commit_url: commit,
2828 | version_url: version_file,
2829 | }
2830 | post = {
2831 | comments_url: {
2832 | 'html_url': comment_html_url,
2833 | },
2834 | statuses_url: {},
2835 | }
2836 | patch = {
2837 | existing_comment_url: {
2838 | 'html_url': comment_html_url,
2839 | },
2840 | }
2841 | delete = {
2842 | existing_added_deleted_comment_url: {
2843 | 'html_url': comment_html_url2,
2844 | },
2845 | }
2846 |
2847 | event = _event(event_data)
2848 |
2849 | gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch, delete=delete)
2850 |
2851 | await router.dispatch(event, gh)
2852 |
2853 | getitem_urls = gh.getitem_urls
2854 | getiter_urls = gh.getiter_urls
2855 | post_urls = gh.post_urls
2856 | post_data = gh.post_data
2857 | patch_urls = gh.patch_urls
2858 | patch_data = gh.patch_data
2859 | delete_urls = gh.delete_urls
2860 |
2861 | assert set(getiter_urls) == set(getiter)
2862 | assert set(getitem_urls) == set(getitem)
2863 | assert post_urls == [statuses_url]
2864 | assert patch_urls == list(patch)
2865 | assert delete_urls == list(delete)
2866 | assert len(post_data) == 1
2867 | assert len(patch_data) == 1
2868 | # Comments data
2869 | assert patch_data[0].keys() == {"body"}
2870 | comment = patch_data[0]["body"]
2871 | assert "release notes" in comment
2872 | assert "\U0001f7e0" not in comment
2873 | assert "add new files" not in comment
2874 | assert "delete files" not in comment
2875 | assert sha not in comment
2876 | assert "`file1`" not in comment
2877 |
--------------------------------------------------------------------------------
/sympy_bot/update_wiki.py:
--------------------------------------------------------------------------------
1 | import urllib
2 | import os
3 | import shlex
4 | import sys
5 | from subprocess import run as subprocess_run, PIPE
6 |
7 | from .changelog import update_release_notes
8 |
9 | # Modified from doctr.travis.run_command_hiding_token
10 | def run(args, shell=False, check=True):
11 | token = os.environ.get("GH_AUTH").encode('utf-8')
12 |
13 | if not shell:
14 | command = ' '.join(map(shlex.quote, args))
15 | else:
16 | command = args
17 |
18 | command = command.replace(token.decode('utf-8'), '~'*len(token))
19 | print(command)
20 | sys.stdout.flush()
21 |
22 | if token:
23 | stdout = stderr = PIPE
24 | else:
25 | stdout = stderr = None
26 | p = subprocess_run(args, stdout=stdout, stderr=stderr, shell=shell, check=check)
27 | if token:
28 | # XXX: Do this in a way that is streaming
29 | out, err = p.stdout, p.stderr
30 | out = out.replace(token, b"~"*len(token))
31 | err = err.replace(token, b"~"*len(token))
32 | if out:
33 | print(out.decode('utf-8'))
34 | if err:
35 | print(err.decode('utf-8'), file=sys.stderr)
36 | sys.stdout.flush()
37 | sys.stderr.flush()
38 | return p.returncode
39 |
40 | def update_wiki(*, wiki_url, release_notes_file, changelogs, pr_number,
41 | authors):
42 | """
43 | Update the release notes wiki with the given release notes
44 |
45 | Assumes the token to push to the wiki is in the GH_AUTH environment variable.
46 |
47 | The git commands and their output are printed to the terminal. The token
48 | is automatically hidden from the output.
49 |
50 | Raises:
51 |
52 | - RuntimeError: If there was an error.
53 | - CalledProcessError: If there was an error with a git command
54 |
55 | Otherwise, it returns None.
56 |
57 | """
58 | run(['git', 'config', '--global', 'user.email', "sympy+bot@sympy.org"])
59 | run(['git', 'config', '--global', 'user.name', "SymPy Bot"])
60 |
61 | run(['git', 'clone', wiki_url, '--depth', '1'], check=True)
62 | _, wiki = wiki_url.rsplit('/', 1)
63 | os.chdir(wiki)
64 |
65 | with open(release_notes_file, 'r') as f:
66 | rel_notes_txt = f.read()
67 |
68 | try:
69 | new_rel_notes_txt = update_release_notes(rel_notes_txt=rel_notes_txt,
70 | changelogs=changelogs, pr_number=pr_number, authors=authors)
71 | except Exception as e:
72 | raise RuntimeError(str(e)) from e
73 |
74 | with open(release_notes_file, 'w') as f:
75 | f.write(new_rel_notes_txt)
76 |
77 | run(['git', 'diff'], check=True)
78 | run(['git', 'add', release_notes_file], check=True)
79 |
80 | message = f"Update {release_notes_file} from PR #{pr_number}"
81 | run(['git', 'commit', '-m', message], check=True)
82 |
83 | parsed_url = list(urllib.parse.urlparse(wiki_url))
84 | parsed_url[1] = os.environ.get("GH_AUTH") + '@' + parsed_url[1]
85 | auth_url = urllib.parse.urlunparse(parsed_url)
86 |
87 | # TODO: Use a deploy key to do this
88 | run(['git', 'push', auth_url, 'master'], check=True)
89 |
--------------------------------------------------------------------------------
/sympy_bot/webapp.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 | import datetime
3 | import os
4 | import base64
5 | from subprocess import CalledProcessError
6 | from collections import defaultdict
7 |
8 | from aiohttp import web, ClientSession
9 |
10 | from gidgethub import routing, sansio, BadRequest
11 | from gidgethub.aiohttp import GitHubAPI
12 |
13 | from .changelog import (get_changelog, update_release_notes, VERSION_RE,
14 | get_release_notes_filename, BEGIN_RELEASE_NOTES,
15 | END_RELEASE_NOTES)
16 | from .update_wiki import update_wiki
17 |
18 | router = routing.Router()
19 |
20 | USER = 'sympy-bot'
21 | RELEASE_FILE = 'sympy/release.py'
22 |
23 | async def main_post(request):
24 | # read the GitHub webhook payload
25 | body = await request.read()
26 |
27 | # our authentication token and secret
28 | secret = os.environ.get("GH_SECRET")
29 | oauth_token = os.environ.get("GH_AUTH")
30 |
31 | # a representation of GitHub webhook event
32 | event = sansio.Event.from_http(request.headers, body, secret=secret)
33 |
34 | print(f"Received {event.event} event with delivery_id={event.delivery_id}")
35 | async with ClientSession() as session:
36 | gh = GitHubAPI(session, USER, oauth_token=oauth_token, cache={})
37 |
38 | # call the appropriate callback for the event
39 | result = await router.dispatch(event, gh)
40 |
41 | return web.Response(status=200, text=str(result))
42 |
43 | async def main_get(request):
44 | oauth_token = os.environ.get("GH_AUTH")
45 |
46 | async with ClientSession() as session:
47 | gh = GitHubAPI(session, USER, oauth_token=oauth_token)
48 | await gh.getitem("/rate_limit")
49 | rate_limit = gh.rate_limit
50 | remaining = rate_limit.remaining
51 | total = rate_limit.limit
52 | reset_datetime = rate_limit.reset_datetime
53 |
54 | return web.Response(status=200, text=f"SymPy Bot has {remaining} of {total} GitHub API requests remaining. They will reset on {reset_datetime} (UTC), which is in {reset_datetime - datetime.datetime.now(datetime.timezone.utc)}.")
55 |
56 | @router.register("pull_request", action="opened")
57 | @router.register("pull_request", action="reopened")
58 | @router.register("pull_request", action="edited")
59 | @router.register("pull_request", action="synchronize")
60 | async def pull_request_edited(event, gh, *args, **kwargs):
61 | pr_number = event.data['pull_request']['number']
62 | print(f"PR #{pr_number} was {event.data['action']}.")
63 | if event.data['pull_request']['state'] == "closed":
64 | print(f"PR #{pr_number} is closed, skipping")
65 | return
66 |
67 | if event.data['pull_request']['user']['login'] == "dependabot[bot]":
68 | await pull_request_noop(event, gh, status_message="This is a Dependabot PR. SymPy Bot not run.")
69 | else:
70 | await pull_request_comment_release_notes(event, gh)
71 | await pull_request_comment_added_deleted(event, gh)
72 | await rate_limit_comment(event, gh)
73 |
74 | async def pull_request_noop(event, gh, status_message=None):
75 | statuses_url = event.data['pull_request']['statuses_url']
76 | await gh.post(statuses_url, data=dict(
77 | state='success',
78 | description=status_message,
79 | context='sympy-bot/release-notes',
80 | ))
81 |
82 | async def pull_request_comment_release_notes(event, gh):
83 | comments_url = event.data["pull_request"]["comments_url"]
84 | number = event.data["pull_request"]["number"]
85 | # TODO: Get the full list of users with commits, not just the user who
86 | # opened the PR.
87 | commits_url = event.data["pull_request"]["commits_url"]
88 | commits = gh.getiter(commits_url)
89 | users = set()
90 | header_in_message = False
91 |
92 | async for commit in commits:
93 | if commit['author']:
94 | users.add(commit['author']['login'])
95 | message = commit['commit']['message']
96 | if BEGIN_RELEASE_NOTES in message or END_RELEASE_NOTES in message:
97 | header_in_message = commit['sha']
98 |
99 | users.add(event.data['pull_request']['head']['user']['login'])
100 |
101 | users = sorted(users)
102 |
103 | contents_url = event.data['pull_request']['base']['repo']['contents_url']
104 | version_url = contents_url.replace('{+path}', RELEASE_FILE)
105 | base_ref = event.data['pull_request']['base']['ref']
106 |
107 | comments = gh.getiter(comments_url)
108 | # Try to find an existing comment to update
109 | existing_comment_release_notes = None
110 | async for comment in comments:
111 | if comment['user']['login'] == USER:
112 | if "release notes entry" in comment['body']:
113 | existing_comment_release_notes = comment
114 |
115 | status, message, changelogs = get_changelog(event.data['pull_request']['body'])
116 |
117 | if status and header_in_message:
118 | status = False
119 | message = f"* The `{BEGIN_RELEASE_NOTES}` / `{END_RELEASE_NOTES}` block should go in the pull request description only, not the commit messages. It was found in the message for the commit {header_in_message}. See https://github.com/sympy/sympy/wiki/Development-workflow#changing-of-commit-messages for information on how to edit commit messages."
120 |
121 | gh_status = 'success' if status else 'failure'
122 |
123 | release_notes_file = "!!ERROR!! Could not get the release notes filename!"
124 | if status:
125 | try:
126 | release_file = await gh.getitem(version_url + f'?ref={base_ref}')
127 | m = VERSION_RE.search(base64.b64decode(release_file['content']).decode('utf-8'))
128 | except BadRequest: # file not found
129 | m = False
130 | if not m:
131 | status = False
132 | gh_status = 'error'
133 | message = f"""\
134 | There was an error getting the version from the `{RELEASE_FILE}` file. Please open an issue at https://github.com/sympy/sympy-bot/issues."""
135 | else:
136 | version = m.group()
137 | release_notes_file = get_release_notes_filename(version)
138 |
139 | status_message = "The release notes look OK" if status else "The release notes check failed"
140 |
141 | emoji_status = {
142 | True: ':white_check_mark:',
143 | False: ':x:',
144 | }
145 |
146 | if status:
147 | fake_release_notes = """
148 | ## Changes
149 |
150 | ## Authors
151 | """
152 | release_notes_url = event.data['pull_request']['base']['repo']['html_url'] + '/wiki/' + release_notes_file[:-3] # Strip the .md for the URL
153 |
154 | try:
155 | updated_fake_release_notes = update_release_notes(rel_notes_txt=fake_release_notes,
156 | changelogs=changelogs, pr_number=number,
157 | authors=users).replace('## Authors', '').replace("## Changes", '').strip()
158 | except Exception as e:
159 | status = False
160 | status_message = "ERROR"
161 | message += f"""
162 |
163 | There was an error processing the release notes, which most likely indicates a bug in the bot. Please open an issue at https://github.com/sympy/sympy-bot/issues. The error was: {e}
164 |
165 | """
166 | else:
167 | if changelogs:
168 | message += f'\n\nHere is what the release notes will look like:\n{updated_fake_release_notes}\n\nThis will be added to {release_notes_url}.'
169 |
170 | release_notes_message = f"""\
171 | {emoji_status[status] if status else ''}
172 |
173 | Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).
174 |
175 | """
176 | if not status:
177 | release_notes_message += f"{emoji_status[status]} There was an issue with the release notes. **Please do not close this pull request;** instead edit the description after reading the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes)."
178 |
179 | release_notes_message += f"""
180 |
181 | {message}
182 |
183 | Click here to see the pull request description that was parsed.
184 |
185 | {textwrap.indent(event.data['pull_request']['body'], ' ')}
186 |
187 |
188 | """ 189 | 190 | if existing_comment_release_notes: 191 | comment = await gh.patch(existing_comment_release_notes['url'], data={"body": release_notes_message}) 192 | else: 193 | comment = await gh.post(comments_url, data={"body": release_notes_message}) 194 | 195 | statuses_url = event.data['pull_request']['statuses_url'] 196 | await gh.post(statuses_url, data=dict( 197 | state=gh_status, 198 | target_url=comment['html_url'], 199 | description=status_message, 200 | context='sympy-bot/release-notes', 201 | )) 202 | 203 | return status, release_notes_file, changelogs, comment, users 204 | 205 | async def rate_limit_comment(event, gh): 206 | comments_url = event.data["pull_request"]["comments_url"] 207 | rate_limit = gh.rate_limit 208 | remaining = rate_limit.remaining 209 | total = rate_limit.limit 210 | reset_datetime = rate_limit.reset_datetime 211 | 212 | if remaining <= 10: 213 | message = f"""\ 214 | 215 | **:warning::warning::warning:WARNING:warning::warning::warning:**: I am nearing my API limit. I have only {remaining} of {total} API requests left. They will reset on {reset_datetime} (UTC), which is in {reset_datetime - datetime.datetime.now(datetime.timezone.utc)}. 216 | 217 | """ 218 | 219 | await gh.post(comments_url, data={"body": message}) 220 | 221 | async def pull_request_comment_added_deleted(event, gh): 222 | comments_url = event.data["pull_request"]["comments_url"] 223 | # TODO: Get the full list of users with commits, not just the user who 224 | # opened the PR. 225 | commits_url = event.data["pull_request"]["commits_url"] 226 | commits = gh.getiter(commits_url) 227 | added = defaultdict(list) 228 | deleted = defaultdict(list) 229 | 230 | async for commit in commits: 231 | # Workaround https://github.com/sympy/sympy-bot/issues/84 232 | try: 233 | com = await gh.getitem(commit['url']) 234 | except BadRequest: 235 | print(f"Warning: could not get commit {commit['sha']}") 236 | continue 237 | if len(com['parents']) > 1: 238 | # Merge commit 239 | continue 240 | for file in com['files']: 241 | if file['status'] == 'added': 242 | added[com['sha']].append(file) 243 | elif file['status'] == 'removed': 244 | deleted[com['sha']].append(file) 245 | 246 | comments = gh.getiter(comments_url) 247 | # Try to find an existing comment to update 248 | existing_comment = None 249 | # mentioned = [] 250 | async for comment in comments: 251 | if comment['user']['login'] == USER: 252 | if "add or delete" in comment['body']: 253 | existing_comment = comment 254 | break 255 | # if f'@{USER}' in comment['body']: 256 | # mentioned.append(comment) 257 | 258 | if added or deleted: 259 | # \U0001f7e0 is an orange circle. Don't include it here literally 260 | # because it causes issues in some editors. We set it as a level 3 261 | # header so it appears the same size as the GitHub :emojis:. It isn't 262 | # available as a :emoji: unfortunately. 263 | added_deleted_message = """\ 264 | ### \U0001f7e0 265 | 266 | Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot). I've noticed that some of your commits add or delete files. Since this is sometimes done unintentionally, I wanted to alert you about it. 267 | 268 | This is an experimental feature of SymPy Bot. If you have any feedback on it, please comment at https://github.com/sympy/sympy-bot/issues/75. 269 | """ 270 | if added: 271 | added_deleted_message += """ 272 | The following commits **add new files**: 273 | """ 274 | for sha, files in added.items(): 275 | added_deleted_message += f"* {sha}:\n" 276 | for file in files: 277 | added_deleted_message += f" - `{file['filename']}`\n" 278 | 279 | if deleted: 280 | added_deleted_message += """ 281 | The following commits **delete files**: 282 | """ 283 | for sha, files in deleted.items(): 284 | added_deleted_message += f"* {sha}:\n" 285 | for file in files: 286 | added_deleted_message += f" - `{file['filename']}`\n" 287 | 288 | added_deleted_message += """ 289 | If these files were added/deleted on purpose, you can ignore this message. 290 | """ 291 | # TODO: Allow users to whitelist files by @mentioning the bot. Then we 292 | # could make this give a failing status. 293 | 294 | if existing_comment: 295 | comment = await gh.patch(existing_comment['url'], data={"body": added_deleted_message}) 296 | else: 297 | comment = await gh.post(comments_url, data={"body": added_deleted_message}) 298 | elif existing_comment: 299 | # Files were added or deleted before but now they aren't, so delete 300 | # the comment 301 | await gh.delete(existing_comment['url']) 302 | 303 | @router.register("pull_request", action="closed") 304 | async def pull_request_closed(event, gh, *args, **kwargs): 305 | pr_number = event.data['pull_request']['number'] 306 | print(f"PR #{pr_number} was {event.data['action']}.") 307 | if not event.data['pull_request']['merged']: 308 | print(f"PR #{pr_number} was closed without merging, skipping") 309 | return 310 | 311 | status, release_notes_file, changelogs, comment, users = await pull_request_comment_release_notes(event, gh, *args, **kwargs) 312 | 313 | wiki_url = event.data['pull_request']['base']['repo']['html_url'] + '.wiki' 314 | release_notes_url = event.data['pull_request']['base']['repo']['html_url'] + '/wiki/' + release_notes_file[:-3] # Strip the .md for the URL 315 | 316 | number = event.data["pull_request"]["number"] 317 | 318 | if status: 319 | if changelogs: 320 | try: 321 | update_wiki( 322 | wiki_url=wiki_url, 323 | release_notes_file=release_notes_file, 324 | changelogs=changelogs, 325 | pr_number=number, 326 | authors=users, 327 | ) 328 | update_message = comment['body'] + f""" 329 | 330 | **Update** 331 | 332 | The release notes on the [wiki]({release_notes_url}) have been updated. 333 | """ 334 | comment = await gh.patch(comment['url'], data={"body": update_message}) 335 | except RuntimeError as e: 336 | await error_comment(event, gh, e.args[0]) 337 | raise 338 | except CalledProcessError as e: 339 | await error_comment(event, gh, str(e)) 340 | raise 341 | else: 342 | print(f"PR #{pr_number} was merged with no change log entries.") 343 | else: 344 | message = "The pull request was merged even though the release notes bot had a failing status." 345 | await error_comment(event, gh, message) 346 | 347 | await rate_limit_comment(event, gh) 348 | 349 | async def error_comment(event, gh, message): 350 | """ 351 | Add a new comment with an error message. For use when updating the release 352 | notes fails. 353 | """ 354 | token = os.environ.get("GH_AUTH") 355 | message = message.replace(token, '~~~TOKEN~~~') 356 | 357 | error_message = f"""\ 358 | 359 | **:rotating_light::rotating_light::rotating_light:ERROR:rotating_light::rotating_light::rotating_light:** There was an error automatically updating the release notes. Normally it should not have been possible to merge this pull request. You might want to open an issue about this at https://github.com/sympy/sympy-bot/issues. 360 | 361 | In the meantime, you will need to update the release notes on the wiki manually. 362 | 363 | The error message was: {message} 364 | """ 365 | 366 | url = event.data["pull_request"]["comments_url"] 367 | comment = await gh.post(url, data={"body": error_message}) 368 | 369 | statuses_url = event.data['pull_request']['statuses_url'] 370 | await gh.post(statuses_url, data=dict( 371 | state='error', 372 | target_url=comment['html_url'], 373 | description='There was an error updating the release notes on the wiki.', 374 | context='sympy-bot/release-notes', 375 | )) 376 | --------------------------------------------------------------------------------