├── pr_consistency
├── common.py
├── pr_checks.sh
├── 3.find_pr_changelog_section.py
├── 2.find_pr_branches.py
├── 1.get_merged_prs.py
├── README.md
└── 4.check_consistency.py
├── update-packages
├── common.py
├── README.md
├── helpers_2.py
├── helpers_3.py
└── update_astropy_helpers.py
├── documents
├── images
│ ├── propose_fork.png
│ ├── propose_commit.png
│ ├── propose_create_1.png
│ └── propose_create_2.png
├── README.md
├── affiliated_package_review_guidelines.md
└── affiliated_package_review_process.md
├── README.md
├── next_pr_number.py
├── ci_helpers_usage.py
├── .gitignore
├── utils.py
├── clean_parse_tables.py
├── LICENSE.rst
├── discontinued_usage
├── astropy_grep_affiliated.py
├── unify_section_headings.py
├── author_lists.py
├── add_to_changelog.py
└── gh_issuereport.py
├── get_travis_builds_info.py
├── common.py
├── astropy_usage.py
├── issue2pr.py
└── visualizations_demographics
├── astropy_status_plots.py
├── cites_and_mentions.py
└── Travis Build Info near Feature Freeze.ipynb
/pr_consistency/common.py:
--------------------------------------------------------------------------------
1 | ../common.py
--------------------------------------------------------------------------------
/update-packages/common.py:
--------------------------------------------------------------------------------
1 | ../common.py
--------------------------------------------------------------------------------
/documents/images/propose_fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/astropy-tools/main/documents/images/propose_fork.png
--------------------------------------------------------------------------------
/documents/images/propose_commit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/astropy-tools/main/documents/images/propose_commit.png
--------------------------------------------------------------------------------
/documents/README.md:
--------------------------------------------------------------------------------
1 | ### About
2 |
3 | This folder is deprecated - for what used to go here, see https://github.com/astropy/project
--------------------------------------------------------------------------------
/documents/images/propose_create_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/astropy-tools/main/documents/images/propose_create_1.png
--------------------------------------------------------------------------------
/documents/images/propose_create_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/astropy-tools/main/documents/images/propose_create_2.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # astropy-procedures
2 | This repository stores scripts for development and advertising, or similar tools used by the astropy project. Some of the tools may be useful in other contexts, but at least right now stability is not guaranteed.
3 |
--------------------------------------------------------------------------------
/next_pr_number.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 | import urllib.request
4 |
5 | if len(sys.argv) == 2:
6 | repository = sys.argv[1]
7 | elif len(sys.argv) > 2:
8 | print("Usage: next_pr_number.py ")
9 | sys.exit(1)
10 | else:
11 | repository = 'astropy/astropy'
12 |
13 | with urllib.request.urlopen(f"https://api.github.com/repos/{repository}/issues?state=all&sort=created&direction=desc") as response:
14 | print(f"Next PR number: {json.loads(response.read())[0]['number'] + 1}")
15 |
--------------------------------------------------------------------------------
/pr_consistency/pr_checks.sh:
--------------------------------------------------------------------------------
1 | if [[ -z $PR_PACKAGE ]]; then
2 | package=astropy/astropy
3 | else
4 | package=${PR_PACKAGE}
5 | fi
6 |
7 | if [[ -z $CHANGELOG ]]; then
8 | changelog=CHANGES.rst
9 | else
10 | changelog=${CHANGELOG}
11 | fi
12 |
13 | if [[ -z $PYEXEC ]]; then
14 | pyexec=python
15 | else
16 | pyexec=${PYEXEC}
17 | fi
18 |
19 | $pyexec 1.get_merged_prs.py ${package} && \
20 | $pyexec 2.find_pr_branches.py ${package} && \
21 | $pyexec 3.find_pr_changelog_section.py ${package} ${changelog} && \
22 | $pyexec 4.check_consistency.py ${package} > consistency.html
23 |
--------------------------------------------------------------------------------
/ci_helpers_usage.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from github import Github
3 | from common import get_credentials
4 |
5 | username, password = get_credentials()
6 | gh = Github(username, password)
7 |
8 | gh_search_result = gh.search_code('filename:.travis.yml "astropy/ci-helpers"')
9 |
10 | gh_repo = []
11 | gh_name = []
12 |
13 | for i in gh_search_result:
14 | gh_repo.append(i.repository.full_name)
15 | gh_name.append(i.repository.name)
16 |
17 | gh_name = set(gh_name)
18 |
19 | pypi_name = []
20 |
21 | for i in gh_name:
22 | response = requests.get("http://pypi.python.org/pypi/{}/json".format(i))
23 | if response.status_code == 200:
24 | pypi_name.append(i)
25 |
26 |
27 | print(len(gh_name), len(pypi_name))
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Script generated files
2 | merged_pull_requests*.json
3 | pull_requests_branches*.json
4 | pull_requests_changelog_sections*.json
5 | pr_consistency/*html
6 |
7 | # Compiled files
8 | *.py[cod]
9 | *.a
10 | *.o
11 | *.so
12 | *.pyd
13 | __pycache__
14 |
15 | # Other generated files
16 | MANIFEST
17 |
18 | # Packages/installer info
19 | *.egg
20 | *.egg-info
21 | dist
22 | build
23 | eggs
24 | .eggs
25 | parts
26 | bin
27 | var
28 | sdist
29 | develop-eggs
30 | .installed.cfg
31 | distribute-*.tar.gz
32 |
33 | # Other
34 | .cache
35 | .tox
36 | .*.swp
37 | *~
38 | .project
39 | .pydevproject
40 | .settings
41 | .coverage
42 | cover
43 | htmlcov
44 |
45 | # Mac OSX
46 | .DS_Store
47 |
48 | # PyCharm
49 | .idea
50 |
51 | # data that might get saved if you run the scripts
52 | issues.json
53 | prs.json
54 |
--------------------------------------------------------------------------------
/update-packages/README.md:
--------------------------------------------------------------------------------
1 | ### Auto-updates for packages using astropy-helpers
2 |
3 | **NOTE: The scripts here are primarily for archival records only.
4 | No further update of astropy-helpers is planned.**
5 |
6 | The scripts in this folder are used to update packages making use of
7 | astropy-helpers by automatically opening pull requests to those packages.
8 |
9 | If you are a package maintainer and would like your package to receive automated
10 | pull requests to update astropy-helpers, then add your package either to
11 | [helpers_2.py](https://github.com/astropy/astropy-procedures/blob/master/update-packages/helpers_2.py)
12 | (if your package supports Python 2) or
13 | [helpers_3.py](https://github.com/astropy/astropy-procedures/blob/master/update-packages/helpers_3.py)
14 | (if your package only supports Python 3).
15 |
--------------------------------------------------------------------------------
/update-packages/helpers_2.py:
--------------------------------------------------------------------------------
1 | # This set of repositories is a mixture of affiliated packages and packages
2 | # that use astropy_helpers and opted in receiving automated PRs to update
3 | # the helpers version. If your package requires Python 3.5+ consider adding
4 | # it to the list in the "helpers_3.py" file to recieve updates for the
5 | # Python 3.5+ releases of astropy-helpers.
6 |
7 | repositories = sorted(set([
8 | ('astropy', 'astroquery'),
9 | ('astropy', 'pyregion'),
10 | ('radio-astro-tools', 'spectral-cube'),
11 | ('astropy', 'astroplan'),
12 | ('astropy', 'astroscrappy'),
13 | ('RiceMunk', 'omnifit'),
14 | ('astropy', 'halotools'),
15 | ('pyspeckit', 'pyspeckit'),
16 | ('linetools', 'linetools'),
17 | ('desihub', 'specsim'),
18 | ('dkirkby', 'speclite'),
19 | ('matiscke', 'lcps')
20 | ]))
21 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | """Utility functions."""
2 |
3 | import sys
4 |
5 | import numpy as np
6 |
7 |
8 | class WWED:
9 | """What Would Erik (Tollerud) Do?
10 |
11 | Examples
12 | --------
13 | >>> import numpy as np
14 | >>> from utils import WWED
15 | >>> np.random.seed(1234)
16 | >>> erik = WWED()
17 | >>> erik.says()
18 | 'You should contribute that to Astropy!'
19 |
20 | """
21 | def __init__(self):
22 | if sys.version_info.major < 3:
23 | raise OSError('Python 2 is not supported')
24 |
25 | self.music_url = 'https://www.youtube.com/watch?v=XAYhNHhxN0A'
26 |
27 | def says(self, suspenseful_music=True):
28 | if suspenseful_music:
29 | import webbrowser
30 | webbrowser.open(self.music_url)
31 |
32 | if np.random.rand() > 0.5:
33 | return "Can you put that in a notebook?"
34 | else:
35 | return "You should contribute that to Astropy!"
36 |
--------------------------------------------------------------------------------
/clean_parse_tables.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | A utility to fix PLY-generated lex and yacc tables to be Python 2 and
5 | 3 compatible.
6 | """
7 | from __future__ import print_function
8 |
9 | import os
10 | import argparse
11 |
12 | parser = argparse.ArgumentParser(description=__doc__)
13 | parser.add_argument('dir', help='The directory to search through for lextab/parsetab files.')
14 | args = parser.parse_args()
15 |
16 | LICENSE_LINE = "# Licensed under a 3-clause BSD style license - see LICENSE.rst"
17 |
18 | for root, dirs, files in os.walk(args.dir):
19 | for fname in files:
20 | if not (fname.endswith('lextab.py') or fname.endswith('parsetab.py')):
21 | continue
22 |
23 | path = os.path.join(root, fname)
24 | print('Found file', path, 'to fix')
25 | with open(path, 'rb') as fd:
26 | lines = fd.readlines()
27 |
28 | with open(path, 'wb') as fd:
29 | if not (lines[0].startswith(LICENSE_LINE)
30 | or lines[1].startswith(LICENSE_LINE)):
31 | print('Re-writing license')
32 | fd.write(LICENSE_LINE + "\n")
33 | fd.write("from __future__ import (absolute_import, division, print_function, unicode_literals)\n")
34 | fd.write('\n')
35 |
36 | lines = [x.replace("u'", "'").replace('u"', '"') for x in lines]
37 | lines = [x for x in lines if not (fname in x and x[0] == '#')]
38 |
39 | fd.write(''.join(lines))
40 |
--------------------------------------------------------------------------------
/LICENSE.rst:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2015, Astropy Developers
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 | * Neither the name of the Astropy Team nor the names of its contributors may be
14 | used to endorse or promote products derived from this software without
15 | specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/discontinued_usage/astropy_grep_affiliated.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Licensed under a 3-clause BSD style license - see LICENSE.rst
3 | # -*- coding: utf-8 -*-
4 |
5 | from __future__ import absolute_import, division, unicode_literals, print_function
6 |
7 | import six
8 | from six.moves.urllib.request import urlopen
9 | from six.moves.urllib.parse import urlencode
10 |
11 | import json
12 | import sys
13 | import webbrowser
14 |
15 |
16 | def get_registry():
17 | req = urlopen("http://astropy.org/affiliated/registry.json")
18 | return json.load(req)
19 |
20 |
21 | def search_astropy_affiliated_packages(args):
22 | search_string = ' '.join(args)
23 |
24 | registry = get_registry()
25 | repos = []
26 | for package in registry['packages']:
27 | repo_url = package.get('repo_url', '')
28 | if repo_url.startswith('http://github.com/'):
29 | if repo_url.endswith('.git'):
30 | repo_url = repo_url[:-4]
31 | repos.append('repo:' + repo_url[len('http://github.com/'):])
32 |
33 | query = ' '.join(repos + [search_string])
34 |
35 | url = 'http://github.com/search?' + urlencode({
36 | 'q': query,
37 | 'type': 'Code'})
38 |
39 | print(url)
40 |
41 | webbrowser.open(url)
42 |
43 |
44 | def main():
45 | if len(sys.argv) == 1 or '--help' in sys.argv:
46 | print("Searches for a string across all affiliated packages on github, ")
47 | print("and opens the results in the default webbrowser.")
48 | print()
49 | print("Usage: astropy_grep_affiliated [SEARCH STRING]")
50 | sys.exit(0)
51 |
52 | search_astropy_affiliated_packages(sys.argv[1:])
53 |
54 |
55 | if __name__ == '__main__':
56 | main()
57 |
--------------------------------------------------------------------------------
/update-packages/helpers_3.py:
--------------------------------------------------------------------------------
1 | # This set of repositories is where the maintainer opted in to use
2 | # astropy_helpers v3.0 and above accepting that it requires Python
3 | # 3.5+. Packages listed below will receive automated update for the latest
4 | # helpers rather than the LTS branch.
5 |
6 | repositories = sorted(set([
7 | ('adrn', 'gala'),
8 | ('aplpy', 'aplpy'),
9 | ('astropy', 'ccdproc'),
10 | ('astropy', 'photutils'),
11 | ('astropy', 'specutils'),
12 | ('astropy', 'reproject'),
13 | ('astropy', 'pyvo'),
14 | ('astropy', 'astrowidgets'),
15 | ('astropy', 'regions'),
16 | ('astropy', 'astropy-healpix'),
17 | ('astropy', 'saba'),
18 | ('ejeschke', 'ginga'),
19 | ('hipspy', 'hips'),
20 | ('sunpy', 'sunpy'),
21 | ('spacetelescope', 'gwcs'),
22 | ('spacetelescope', 'specviz'),
23 | ('spacetelescope', 'stginga'),
24 | ('spacetelescope', 'wss_tools'),
25 | ('spacetelescope', 'astroimtools'),
26 | ('dkirkby', 'skysim'),
27 | ('dkirkby', 'speclite'),
28 | ('desihub', 'specsim'),
29 | ('plasmapy', 'plasmapy'),
30 | ('karllark', 'DGFit'),
31 | ('mhvk', 'baseband'),
32 | ('mhvk', 'scintillometry'),
33 | ('hamogu', 'arcus'),
34 | ('hamogu', 'marxs-lynx'),
35 | ('hamogu', 'psfsubtraction'),
36 | ('Chandra-MARX', 'marxs'),
37 | ('StingraySoftware', 'stingray'),
38 | ('StingraySoftware', 'HENDRICS'),
39 | ('discos', 'srt-single-dish-tools'),
40 | ('NASA-Planetary-Science', 'sbpy'),
41 | ('weaverba137', 'pydl'),
42 | ('BEAST-Fitting', 'beast'),
43 | ('BEAST-Fitting', 'megabeast'),
44 | ('PAHFIT', 'pahfit'),
45 | ('karllark', 'dust_extinction'),
46 | ('karllark', 'dust_attenuation'),
47 | ('karllark', 'measure_extinction'),
48 | ('ehsteve', 'roentgen')
49 | ]))
50 |
--------------------------------------------------------------------------------
/get_travis_builds_info.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import tqdm
4 | import math
5 | import json
6 | import urllib
7 | import argparse
8 | import requests
9 |
10 | def get_travis_build_info(reponame, token, fail_output_fn=None):
11 | headers = {'Travis-API-Version':'3', 'Authorization':'token ' + token}
12 |
13 | slug = urllib.parse.urlencode({'':reponame})[1:]
14 | base_url = 'https://api.travis-ci.org'
15 |
16 | start_url = '/repo/{}/builds?limit=100'.format(slug)
17 | res = requests.get(base_url + start_url, headers=headers)
18 | if not res.ok:
19 | raise ValueError('first request was not 200/OK - returned "{}"',format(res.text), res, res.headers)
20 |
21 | j = res.json()
22 | builds = j['builds']
23 |
24 | npages = math.ceil(j['@pagination']['count']/j['@pagination']['limit'])
25 | try:
26 | with tqdm.tqdm(total=npages) as pbar:
27 | while j['@pagination']['next'] is not None:
28 | res = requests.get(base_url + j['@pagination']['next']['@href'], headers=headers)
29 | pbar.update(1)
30 | if not res.ok:
31 | raise ValueError('Request was not 200/OK - returned "{}"',format(res.text), res, res.headers)
32 | j = res.json()
33 | builds.extend(j['builds'])
34 | pbar.update(1) # should get to 100%
35 | finally:
36 | if fail_output_fn:
37 | with open(fail_output_fn, 'w') as f:
38 | json.dump(builds, f)
39 |
40 |
41 | return builds
42 |
43 | if __name__ == '__main__':
44 | p = argparse.ArgumentParser()
45 |
46 | p.add_argument('reponame')
47 | p.add_argument('token')
48 | p.add_argument('-o', '--output-file', default='travis_build_info.json')
49 | p.add_argument('--no-gzip', action='store_true')
50 |
51 | args = p.parse_args()
52 | builds = get_travis_build_info(args.reponame, args.token, args.output_file + '.fail')
53 |
54 | if builds:
55 | fn = args.output_file
56 | if not args.no_gzip:
57 | import gzip
58 | fopen = gzip.open
59 | if not fn.endswith('.gz'):
60 | fn += '.gz'
61 | fobj = gzip.open(fn, 'wt')
62 | else:
63 | fobj = open(fn, 'w')
64 | try:
65 | json.dump(builds, fobj)
66 | finally:
67 | fobj.close()
68 |
--------------------------------------------------------------------------------
/pr_consistency/3.find_pr_changelog_section.py:
--------------------------------------------------------------------------------
1 | # The purpose of this script is to search through the latest changelog to find
2 | # for each pull request what section of the changelog the pull request is
3 | # mentioned in. The output is a JSON file that contains for each pull request
4 | # the changelog section.
5 |
6 | import os
7 | import re
8 | import sys
9 | import json
10 | import tempfile
11 |
12 | import requests
13 |
14 | if sys.argv[1:]:
15 | REPOSITORY = sys.argv[1]
16 | else:
17 | REPOSITORY = 'astropy/astropy'
18 |
19 | if sys.argv[2:]:
20 | CHANGELOG_NAME = sys.argv[2]
21 | else:
22 | CHANGELOG_NAME = 'CHANGES.rst'
23 |
24 | NAME = os.path.basename(REPOSITORY)
25 |
26 | print("The repository this script currently works with is '{}'.\n"
27 | .format(REPOSITORY))
28 |
29 | if os.environ.get('LOCAL_CHANGELOG'):
30 | CHANGELOG = os.environ['LOCAL_CHANGELOG']
31 | with open(CHANGELOG) as f:
32 | changelog_lines = f.readlines()
33 | else:
34 | CHANGELOG = f'https://raw.githubusercontent.com/{REPOSITORY}/master/{CHANGELOG_NAME}'
35 | changelog_lines = requests.get(CHANGELOG).text.splitlines()
36 |
37 | TMPDIR = tempfile.mkdtemp()
38 |
39 | BLOCK_PATTERN = re.compile('[[(]#[0-9#, ]+[])]')
40 | ISSUE_PATTERN = re.compile('#[0-9]+')
41 |
42 |
43 | def find_prs_in_changelog(content):
44 | issue_numbers = []
45 | for block in BLOCK_PATTERN.finditer(content):
46 | block_start, block_end = block.start(), block.end()
47 | block = content[block_start:block_end]
48 |
49 | for m in ISSUE_PATTERN.finditer(block):
50 | start, end = m.start(), m.end()
51 | issue_numbers.append(block[start:end][1:])
52 | return issue_numbers
53 |
54 | # Get all the PR numbers from the changelog
55 |
56 |
57 | changelog_prs = {}
58 | version = None
59 | content = ''
60 | previous = None
61 |
62 | new_changelog_format = False
63 |
64 | for line in changelog_lines:
65 | if '=======' in line:
66 | new_changelog_format = True
67 | if '=======' in line or (not new_changelog_format and '-------' in line):
68 | if version is not None:
69 | for pr in find_prs_in_changelog(content):
70 | changelog_prs[pr] = version
71 | version = previous.strip().split('(')[0].strip()
72 | if 'v' not in version:
73 | version = 'v' + version
74 | content = ''
75 |
76 | elif version is not None:
77 | content += line
78 | previous = line
79 |
80 | with open(f'pull_requests_changelog_sections_{NAME}.json', 'w') as f:
81 | json.dump(changelog_prs, f, sort_keys=True, indent=2)
82 |
--------------------------------------------------------------------------------
/common.py:
--------------------------------------------------------------------------------
1 | import netrc
2 | import getpass
3 | import warnings
4 |
5 | GITHUB_API_HOST = 'api.github.com'
6 |
7 | BRANCHES_DICT = {'astropy/astropy': ['v0.1.x', 'v0.2.x', 'v0.3.x', 'v0.4.x',
8 | 'v1.0.x', 'v1.1.x', 'v1.2.x', 'v1.3.x',
9 | 'v2.0.x',
10 | 'v3.0.x', 'v3.1.x', 'v3.2.x',
11 | 'v4.0.x', 'v4.1.x', 'v4.2.x', 'v4.3.x'],
12 | 'astropy/astropy-helpers': ['v0.4.x', 'v1.0.x', 'v1.1.x',
13 | 'v1.2.x', 'v1.3.x',
14 | 'v2.0.x',
15 | 'v3.0.x', 'v3.1.x', 'v3.2.x',
16 | 'v4.0.x'],
17 | 'astropy/astroquery': [], # we don't have bugfix branches
18 | }
19 |
20 |
21 | def get_credentials(username=None, password=None, needs_token=False):
22 | pwtype = 'personal access token' if needs_token else 'password'
23 |
24 | try:
25 | my_netrc = netrc.netrc()
26 | except Exception:
27 | pass
28 | else:
29 | auth = my_netrc.authenticators(GITHUB_API_HOST)
30 | if auth:
31 | response = 'NONE' # to allow enter to be default Y
32 | while response.lower() not in ('y', 'n', ''):
33 | print('Using the following GitHub credentials from '
34 | '~/.netrc: {}/{}'.format(auth[0], '*' * 8))
35 | response = input(
36 | 'Use these credentials (if not you will be prompted '
37 | 'for new credentials)? [Y/n] ')
38 | if response.lower() == 'y' or response == '':
39 | username = auth[0]
40 | password = auth[2]
41 | if needs_token:
42 | warnings.warn('Interpreting "password" in netrc as a personal access token')
43 |
44 | if not (username or password):
45 | print(f"Enter your GitHub username and {pwtype} so that API "
46 | "requests aren't as severely rate-limited...")
47 | username = input('Username: ')
48 | password = getpass.getpass('Password: ')
49 | elif not password:
50 | print(f"Enter your GitHub {pwtype} so that API "
51 | "requests aren't as severely rate-limited...")
52 | password = getpass.getpass('Password: ')
53 |
54 | return username, password
55 |
56 |
57 | def get_branches(repo):
58 | try:
59 | branches = BRANCHES_DICT[repo]
60 | except KeyError:
61 | print("No branches of interest was defined, using all branches with "
62 | "names starting with a number or v[0-9] ")
63 |
64 | from github import Github
65 | from common import get_credentials
66 | g = Github(*get_credentials())
67 | repo = g.get_repo(repo)
68 |
69 | branches = []
70 |
71 | for br in repo.get_branches():
72 | if (br.name[0] in '1234567890'
73 | or br.name[0] == 'v' and br.name[1] in '1234567890'):
74 | branches.append(br.name)
75 |
76 | return branches
77 |
--------------------------------------------------------------------------------
/discontinued_usage/unify_section_headings.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Find the sets of characters used in RST section headers,
5 | and replace with a standard sequcne
6 |
7 | For input like
8 | ::
9 |
10 | ##########
11 | header
12 | ##########
13 |
14 | Section
15 | ********
16 |
17 | Subsection
18 | ===========
19 |
20 | Section 2
21 | **********
22 |
23 | and HEADER_CHAR_LEVELS = '*=-^"+:~'
24 |
25 | this should produce
26 | ::
27 |
28 | **********
29 | header
30 | **********
31 |
32 | Section
33 | ========
34 |
35 | Subsection
36 | -----------
37 |
38 | Section 2
39 | ==========
40 |
41 |
42 | It also checks that the ordering is valid.
43 |
44 | Usage::
45 |
46 | % find . -name "*.rst" | xargs ./replace_header_chars.py
47 |
48 | """
49 |
50 | import re
51 | import shutil
52 | import sys
53 | import tempfile
54 |
55 |
56 | HEADER_CHAR_LEVELS = '*=-^"+:~'
57 |
58 |
59 | def replace_header_chars(filename):
60 | header_chars = ''
61 | level = -1
62 |
63 | rx = re.compile(r'^(\S)\1{3,}\s*$')
64 | with open(filename, 'r') as infile, tempfile.NamedTemporaryFile(suffix='.rst', delete=False) as outfile:
65 | for i, line in enumerate(infile):
66 | match = rx.match(line)
67 | if match:
68 | char = match.group(1)
69 | if char in header_chars:
70 | # Existing header char: go back out to that level
71 | new_level = header_chars.index(char)
72 | if new_level > level + 1:
73 | # doc tries to jump up too many levels
74 | raise ValueError(
75 | 'ERROR misorder new_level={} level={} '
76 | 'char={} header_chars={} on line {}'.format(
77 | new_level, level, char, header_chars, i))
78 | else:
79 | level = new_level
80 | print('s/{}/{}/'.format(char, HEADER_CHAR_LEVELS[level]))
81 | else:
82 | # New header char - create a deeper level
83 | if level == len(header_chars) - 1:
84 | header_chars += char
85 | level += 1
86 |
87 | print('s/{}/{}/'.format(char, HEADER_CHAR_LEVELS[level]))
88 | else:
89 | # we're trying to create a new level, but we're not at the current deepest level
90 | raise ValueError(
91 | 'ERROR misorder {} at level {} from {} on line {}'.format(
92 | char, level, header_chars, i))
93 | outfile.write(line.replace(char, HEADER_CHAR_LEVELS[level]).encode())
94 | else:
95 | outfile.write(line.encode())
96 |
97 | print('{} -> {}'.format(outfile.name, filename))
98 | shutil.move(outfile.name, filename)
99 |
100 | return header_chars
101 |
102 |
103 | def main(argv=None):
104 | for filename in argv:
105 | replace_header_chars(filename)
106 |
107 |
108 | if __name__ == '__main__':
109 | sys.exit(main(sys.argv[1:]))
110 |
--------------------------------------------------------------------------------
/pr_consistency/2.find_pr_branches.py:
--------------------------------------------------------------------------------
1 | # The purpose of this script is to check all the maintenance branches of the
2 | # given repository, and find which pull requests are included in which
3 | # branches. The output is a JSON file that contains for each pull request the
4 | # list of all branches in which it is included. We look specifically for the
5 | # message "Merge pull request #xxxx " in commit messages, so this is not
6 | # completely foolproof, but seems to work for now.
7 |
8 | import os
9 | import sys
10 | import json
11 | import re
12 | import subprocess
13 | import tempfile
14 | from collections import defaultdict
15 |
16 | from astropy.utils.console import color_print
17 |
18 | from common import get_branches
19 |
20 | if sys.argv[1:]:
21 | REPOSITORY_NAME = sys.argv[1]
22 | else:
23 | REPOSITORY_NAME = 'astropy/astropy'
24 |
25 | print("The repository this script currently works with is '{}'.\n"
26 | .format(REPOSITORY_NAME))
27 |
28 |
29 | REPOSITORY = f'git://github.com/{REPOSITORY_NAME}.git'
30 | NAME = os.path.basename(REPOSITORY_NAME)
31 |
32 | DIRTOCLONEIN = tempfile.mkdtemp() # set this to a non-temp directory to retain the clone between runs
33 | ORIGIN = 'origin' # set this to None to not fetch anything but rather use the directory as-is.
34 |
35 | STARTDIR = os.path.abspath('.')
36 |
37 | # The branches we are interested in
38 | BRANCHES = get_branches(REPOSITORY_NAME)
39 |
40 | # Read in a list of all the PRs
41 | with open(f'merged_pull_requests_{NAME}.json') as merged:
42 | merged_prs = json.load(merged)
43 |
44 | # Set up a dictionary where each key will be a PR and each value will be a list
45 | # of branches in which the PR is present
46 | pr_branches = defaultdict(list)
47 |
48 |
49 | try:
50 | # Set up repository
51 | color_print(f'Cloning {REPOSITORY}', 'green')
52 | os.chdir(DIRTOCLONEIN)
53 | if os.path.isdir(NAME):
54 | # already exists... assume its the right thing
55 | color_print('"{}" directory already exists - assuming it is an already '
56 | 'existing clone'.format(NAME), 'yellow')
57 | os.chdir(NAME)
58 | if ORIGIN:
59 | subprocess.call(f'git fetch {ORIGIN}', shell=True)
60 | else:
61 | subprocess.call(f'git clone {REPOSITORY}', shell=True)
62 | os.chdir(NAME)
63 |
64 | # Loop over branches and find all PRs in the branch
65 | for branch in BRANCHES:
66 |
67 | # Change branch
68 | color_print(f'Switching to branch {branch}', 'green')
69 | subprocess.call('git reset --hard', shell=True)
70 | subprocess.call('git clean -fxd', shell=True)
71 | subprocess.call(f'git checkout {branch}', shell=True)
72 | if ORIGIN:
73 | subprocess.call(f'git reset --hard {ORIGIN}/{branch}', shell=True)
74 |
75 | # Extract log:
76 | log = subprocess.check_output('git log', shell=True).decode('utf-8')
77 |
78 | # Check for the presence of the PR in the log
79 | for pr in (re.findall(r'Merge pull request #(\d+) ', log) +
80 | re.findall(r'Backport PR #(\d+):', log)):
81 | pr_branches[pr].append(branch)
82 |
83 | finally:
84 | os.chdir(STARTDIR)
85 |
86 | with open(f'pull_requests_branches_{NAME}.json', 'w') as f:
87 | json.dump(pr_branches, f, sort_keys=True, indent=2)
88 |
--------------------------------------------------------------------------------
/discontinued_usage/author_lists.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | A script for generating contributor lists for astropy.
5 |
6 | Note that this requires GitPython (https://gitpython.readthedocs.org).
7 | """
8 | from __future__ import print_function, division
9 |
10 | import os
11 | from git import Repo
12 |
13 | def log_repos(repos, logformat, moreargs=None, append_repo_name=False):
14 | for repodir in repos:
15 | if not os.path.isdir(repodir):
16 | raise ValueError('{} is not a directory!'.format(repodir))
17 |
18 | repodct = {}
19 | for repodir in repos:
20 | logargs = ["--format=format:{0}".format(logformat)]
21 | if append_repo_name:
22 | logargs[0] = logargs[0] + ', repodir=' + repodir
23 | logargs[0] = logargs[0]+''
24 | if moreargs:
25 | logargs.extend(moreargs)
26 |
27 |
28 | repo = Repo(repodir)
29 | logout = repo.git.log(*logargs)
30 | if logout.endswith(''):
31 | logout = logout[:-5]
32 | repodct[repodir] = logout.split('\n')
33 | return repodct
34 |
35 |
36 | def get_long_logs(repos):
37 | longerlogs = log_repos(repos, "%h, %ad, %aN, %an, %ae", append_repo_name=True)
38 | outlines = []
39 | for repo in repos:
40 | outlines.extend(longerlogs[repo])
41 | return outlines
42 |
43 |
44 | if __name__ == '__main__':
45 | import argparse
46 |
47 | parser = argparse.ArgumentParser(description=__doc__)
48 |
49 | parser.add_argument('repos', help='local repositories to search for authors (paths to directories',
50 | nargs='+')
51 | parser.add_argument('-n', '--no-names', action='store_true')
52 | parser.add_argument('-o', '--output-file', default=None)
53 | parser.add_argument('-b', '--bullets', action='store_true')
54 | parser.add_argument('-t', '--html', action='store_true')
55 | parser.add_argument('-m', '--mailmap-info', action='store_true', help='include a commit history useful for populating the mailmap for the repos.')
56 | parser.add_argument('-l', '--last-name', action='store_true', help='sort on last name')
57 |
58 | args = parser.parse_args()
59 |
60 | namedct = log_repos(args.repos, '%aN')
61 |
62 | names = []
63 | for namelist in namedct.values():
64 | names.extend(namelist)
65 |
66 | unames = set(names)
67 | if args.last_name:
68 | unames = sorted(set(names), key=lambda n:n.split()[-1])
69 | else:
70 | unames = sorted(set(names))
71 |
72 |
73 | outlines = []
74 |
75 | if not args.no_names:
76 | outlines.append('----NAMES----')
77 | outlines.extend(unames)
78 |
79 | if args.bullets:
80 | outlines.append('----BULLETS----')
81 | for n in unames:
82 | outlines.append('* '+n)
83 |
84 | if args.html:
85 | outlines.append('----HTML----')
86 | for n in unames:
87 | outlines.append('\t\t\t' + n + '')
88 |
89 | if args.mailmap_info:
90 | outlines.append('----MAILMAP_COMMIT_LOG----')
91 | outlines.extend(get_long_logs(args.repos))
92 |
93 | output = '\n'.join(outlines)
94 | if args.output_file is None:
95 | print(output)
96 | else:
97 | with open(args.output_file, 'w') as f:
98 | f.write(output)
99 |
--------------------------------------------------------------------------------
/astropy_usage.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import pickle
4 | import requests
5 |
6 | from github import Github, GithubException
7 |
8 | from common import get_credentials
9 |
10 | step_size = 100
11 | search_phrase = '"from astropy" import OR "import astropy"'
12 |
13 | username, password = get_credentials()
14 |
15 | gh = Github(username, password)
16 | total_repo = gh.search_code(search_phrase).totalCount
17 |
18 |
19 | if len(sys.argv) > 1:
20 | filename = sys.argv[1]
21 | print("Loading previous results from {}".format(filename))
22 | with open(filename, 'rb') as f:
23 | saved_results = pickle.load(f)
24 | step, queried_results, gh_repo, gh_name, missed_results = saved_results
25 | else:
26 | step = 0
27 | queried_results = 0
28 | gh_repo = set()
29 | gh_name = set()
30 | missed_results = 0
31 |
32 | print("Total number of search results: {}".format(total_repo))
33 |
34 |
35 | # We need to limit the search by repo size as the results are limited to 1000.
36 | # (and astropy is imported in 58K+ repositories.
37 |
38 | while queried_results + missed_results < total_repo:
39 | queried_results_rollback = queried_results
40 | gh_search_result = gh.search_code('{} size:{}..{}'.format(
41 | search_phrase, step * step_size, (step + 1) * step_size))
42 | current_total = gh_search_result.totalCount
43 |
44 | if current_total > 1000:
45 | print("Use smaller step_size total count is {} for size {}..{}".format(
46 | current_total, step * step_size, (step + 1) * step_size))
47 | missed_results += current_total - 1000
48 |
49 | try:
50 | for i in gh_search_result:
51 | gh_full_name = i.repository.full_name
52 | gh_repo.add(gh_full_name)
53 | gh_name.add(gh_full_name.split('/')[-1])
54 | queried_results += 1
55 | # This is an ugly hack to work around the API limits
56 | time.sleep(0.4)
57 | except GithubException:
58 | print("Finished search up until step {} for a total of {} repos. "
59 | "Search failed after the {}th repo.".format(
60 | step, queried_results_rollback, queried_results))
61 | queried_results = queried_results_rollback
62 | with open('usage_results.pkl', 'wb') as f:
63 | results = (step, queried_results, gh_repo, gh_name, missed_results)
64 | pickle.dump(results, f)
65 | raise
66 |
67 | step += 1
68 |
69 | # save partial results at each step
70 | with open('usage_results.pkl', 'wb') as f:
71 | results = (step, queried_results, gh_repo, gh_name, missed_results)
72 | pickle.dump(results, f)
73 |
74 | time.sleep(30)
75 |
76 |
77 | pypi_name = set()
78 | checked_names = set()
79 |
80 | for i, name in enumerate(gh_name):
81 | response = requests.get("http://pypi.python.org/pypi/{}/json".format(name))
82 | if response.status_code == 200:
83 | pypi_name.add(name)
84 | checked_names.add(name)
85 |
86 | # Save partial results
87 | if i % 500 == 0:
88 | with open('pypi_results.pkl', 'wb') as f:
89 | results = (pypi_name, checked_names)
90 | pickle.dump(results, f)
91 |
92 | with open('pypi_results.pkl', 'wb') as f:
93 | results = (pypi_name, checked_names)
94 | pickle.dump(results, f)
95 |
96 | print("Unique GitHub repos: {}\n"
97 | "Projects on PyPI: {}\n".format(
98 | len(gh_repo), len(pypi_name)))
99 |
--------------------------------------------------------------------------------
/pr_consistency/1.get_merged_prs.py:
--------------------------------------------------------------------------------
1 | # The purpose of this script is to download information about all pull
2 | # requests merged into the master branch of the given repository. This
3 | # information is downloaded to a JSON file.
4 |
5 | import os
6 | import sys
7 | import json
8 | import requests
9 |
10 | from common import get_credentials
11 |
12 | QUERY_TEMPLATE = """
13 | {{
14 | repository(owner: "{owner}", name: "{repository}") {{
15 | pullRequests(first:100, orderBy: {{direction: ASC, field: CREATED_AT}}, baseRefName: "{basename}", states: MERGED{after}) {{
16 | edges {{
17 | node {{
18 | title
19 | number
20 | mergeCommit {{
21 | oid
22 | }}
23 | createdAt
24 | updatedAt
25 | mergedAt
26 | milestone {{
27 | title
28 | }}
29 | labels(first: 10) {{
30 | edges {{
31 | node {{
32 | name
33 | }}
34 | }}
35 | }}
36 | }}
37 | cursor
38 | }}
39 | }}
40 | }}
41 | }}
42 | """
43 |
44 | if sys.argv[1:]:
45 | REPOSITORY = sys.argv[1]
46 | else:
47 | REPOSITORY = 'astropy/astropy'
48 |
49 | OWNER = os.path.dirname(REPOSITORY)
50 | NAME = os.path.basename(REPOSITORY)
51 |
52 | print("The repository this script currently works with is '{}'.\n"
53 | .format(REPOSITORY))
54 |
55 | json_filename = f'merged_pull_requests_{NAME}.json'
56 |
57 | TOKEN = get_credentials('N/A', needs_token=True)[1]
58 |
59 | headers = {"Authorization": f"Bearer {TOKEN}"}
60 |
61 | cursor = None
62 |
63 | pull_requests = {}
64 |
65 | try:
66 | for basename in ('master', 'main'):
67 | print('Searching for PRs into branch', basename)
68 |
69 | entries = True
70 | while entries:
71 | print('cursor:', cursor)
72 |
73 | if cursor is None:
74 | after = ''
75 | else:
76 | after = f', after:"{cursor}"'
77 |
78 | query = QUERY_TEMPLATE.format(owner=OWNER, repository=NAME, after=after, basename=basename)
79 |
80 | request = requests.post('https://api.github.com/graphql', json={'query': query}, headers=headers)
81 |
82 | if request.status_code != 200:
83 | raise Exception("Query failed")
84 |
85 | entries = request.json()['data']['repository']['pullRequests']['edges']
86 |
87 | for entry in entries:
88 |
89 | pr = entry['node']
90 | cursor = entry['cursor']
91 |
92 | pull_requests[str(pr['number'])] = {'milestone': pr['milestone']['title'] if pr['milestone'] else None,
93 | 'title': pr['title'],
94 | 'labels': [edge['node']['name'] for edge in pr['labels']['edges']],
95 | 'merged': pr['mergedAt'].replace('Z', ''),
96 | 'updated': pr['updatedAt'].replace('Z', ''),
97 | 'created': pr['createdAt'].replace('Z', ''),
98 | 'merge_commit': pr['mergeCommit']['oid'] if pr['mergeCommit'] else None}
99 |
100 | finally:
101 | with open(json_filename, 'w') as f:
102 | json.dump(pull_requests, f, sort_keys=True, indent=2)
103 |
--------------------------------------------------------------------------------
/discontinued_usage/add_to_changelog.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 |
4 | """
5 | A script to generate a changelog section for a new version of astropy.
6 | Use like:
7 |
8 | $ python add_to_changelog.py ../astropy/CHANGES.rst v1.2
9 |
10 | And it will print out a new changelog section for v1.2.
11 |
12 | Note that this uses the CHANGES.rst that's the first argument to figure out what
13 | all the sections are that should go in the new changelog. So be sure it points
14 | to a CHANGES.rst that's up-to-date with all the sections the new version should
15 | have.
16 | """
17 |
18 | import re
19 | import argparse
20 |
21 |
22 | NEW_CHANGELOG_TEMPLATE = """{newvers} (unreleased)
23 | {newvers_head}-------------
24 |
25 | New Features
26 | ^^^^^^^^^^^^
27 |
28 | {package_list}
29 |
30 | API Changes
31 | ^^^^^^^^^^^
32 |
33 | {package_list}
34 |
35 | Bug Fixes
36 | ^^^^^^^^^
37 |
38 | {package_list}
39 |
40 | Other Changes and Additions
41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 |
43 | - Nothing changed yet.
44 |
45 | """
46 |
47 |
48 | new_version_re = re.compile(r'--*$')
49 | package_re = re.compile(r'- ``(astropy\..*)``$')
50 |
51 | def find_all_package_sections(fn):
52 | pkg_list = []
53 |
54 | firstvers = False
55 | with open(fn) as f:
56 | for l in f:
57 | if new_version_re.match(l):
58 | # this lets us go through exactly one version
59 | if firstvers:
60 | break
61 | else:
62 | firstvers = True
63 |
64 | m = package_re.match(l)
65 | if m:
66 | pnm = m.group(1)
67 | if pnm not in pkg_list:
68 | pkg_list.append(pnm)
69 |
70 | return pkg_list
71 |
72 |
73 | def main(argv=None):
74 | parser = argparse.ArgumentParser(description='Print out the changelog for '
75 | 'a new version of Astropy.')
76 | parser.add_argument('changelog', help='path to the changelog to use as a '
77 | 'template')
78 | parser.add_argument('version', help='the new version to generate the '
79 | 'changelog for')
80 | parser.add_argument('--write', '-w', action='store_true', help='update the '
81 | 'referenced changelog (the first '
82 | 'argument) to include the generated '
83 | 'changelog at the top (or if -l is '
84 | 'set, it determines the location).')
85 | parser.add_argument('--last-version', '-l', help='The name of the version '
86 | 'that this changelog entry should precede. If not '
87 | 'given, this changelog will go at the top', default='')
88 |
89 | args = parser.parse_args(argv)
90 |
91 | pkg_list = find_all_package_sections(args.changelog)
92 |
93 | pkg_list_str = '\n\n'.join(['- ``{}``'.format(pnm) for pnm in pkg_list])
94 |
95 |
96 | new_changelog = NEW_CHANGELOG_TEMPLATE.format(newvers=args.version,
97 | newvers_head='-'*len(args.version),
98 | package_list=pkg_list_str)
99 | if args.write:
100 | with open(args.changelog) as fr:
101 | old_changelog = fr.read()
102 | with open(args.changelog, 'w') as fw:
103 | if args.last_version:
104 | header_match = None
105 | for l in old_changelog.split('\n'):
106 | if args.last_version + ' (' in l:
107 | header_match = l + '\n'
108 | elif header_match:
109 | if '-'*len(args.last_version) in l:
110 | fw.write('\n\n')
111 | fw.write(new_changelog)
112 | fw.write('\n\n\n')
113 | fw.write(header_match)
114 | fw.write(l)
115 | fw.write('\n')
116 | header_match = None
117 | else:
118 | fw.write(l)
119 | fw.write('\n')
120 | else:
121 | fw.write(new_changelog)
122 | fw.write('\n\n\n')
123 | fw.write(old_changelog)
124 |
125 | print(new_changelog)
126 |
127 | if __name__ == '__main__':
128 | main()
129 |
--------------------------------------------------------------------------------
/issue2pr.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #Requires python >=2.6
4 | """
5 | Tool to convert github issues to pull requests by attaching code. Uses the
6 | github v3 API. Defaults assumed for the `astropy `_
7 | project.
8 | """
9 |
10 | from __future__ import print_function
11 |
12 | import argparse
13 | import json
14 | import sys
15 |
16 | from common import get_credentials
17 |
18 | try:
19 | import requests
20 | except ImportError:
21 | print('This script requests the requests module--it can be installed '
22 | '`pip install requests`, or your package manager of choice.',
23 | file=sys.stderr)
24 | sys.exit(1)
25 |
26 |
27 | if sys.version_info[0] >= 3:
28 | # Define raw_input on Python 3
29 | raw_input = input
30 | from urllib.parse import urljoin as basejoin
31 | else:
32 | from urllib import basejoin
33 |
34 |
35 | GITHUB_API_HOST = 'api.github.com'
36 | GITHUB_API_BASE_URL = 'https://{0}/repos/'.format(GITHUB_API_HOST)
37 |
38 |
39 | def issue_to_pr(issuenum, srcbranch, repo='astropy', sourceuser='',
40 | targetuser='astropy', targetbranch='master',
41 | baseurl=GITHUB_API_BASE_URL):
42 | """
43 | Attaches code to an issue, converting a regular issue into a pull request.
44 |
45 | Parameters
46 | ----------
47 | issuenum : int
48 | The issue number (in `targetuser`/`repo`) onto which the code should be
49 | attached.
50 | srcbranch : str
51 | The branch (in `username`/`repo`) the from which to attach the code.
52 | After this is complete, further updates to this branch will be passed
53 | on to the pull request.
54 | repo : str
55 | The name of the repository (the same-name repo should be present for
56 | both `targetuser` and `username`).
57 | targetuser : str
58 | The name of the user/organization that has the issue.
59 | targetbranch : str
60 | The name of the branch (in `targetuser`/`repo`) that the pull request
61 | should merge into.
62 | baseurl : str
63 | The URL to use to access the github site (including protocol).
64 |
65 | .. warning::
66 | Be cautious supplying `pw` as a string - if you do this in an ipython
67 | session, for example, it will be logged in the input history, revealing
68 | your password in clear text. When in doubt, leave it as `None`,
69 | as this will securely prompt you for your password.
70 |
71 | Returns
72 | -------
73 | response : str
74 | The json-decoded response from the github server.
75 | error message : str, optional
76 | If present, indicates github responded with an HTTP error.
77 |
78 | """
79 |
80 | while not sourceuser:
81 | sourceuser = raw_input('Enter GitHub username to create pull request '
82 | 'from: ').strip()
83 |
84 | username, password = get_credentials(username=sourceuser)
85 |
86 | data = {'issue': str(issuenum),
87 | 'head': sourceuser + ':' + srcbranch,
88 | 'base': targetbranch}
89 |
90 | datajson = json.dumps(data)
91 |
92 | suburl = '{user}/{repo}/pulls'.format(user=targetuser, repo=repo)
93 | url = basejoin(baseurl, suburl)
94 | res = requests.post(url, data=datajson, auth=(username, password))
95 | return res.json()
96 |
97 |
98 | def main(argv=None):
99 | if sys.version_info < (2,6):
100 | print('issue2pr.py requires Python >=2.6, exiting')
101 | sys.exit(-1)
102 |
103 | descr = 'Convert a github issue to a Pull Request by attaching code.'
104 | parser = argparse.ArgumentParser(description=descr)
105 |
106 | parser.add_argument('srcbranch', metavar='BRANCH', type=str, help='The '
107 | 'name of the branch in the source (user\'s) repository '
108 | 'that is to be pulled into the target.')
109 | parser.add_argument('issuenum', metavar='ISSUE', type=int,
110 | help='The issue number from the target repository')
111 | parser.add_argument('--repo', metavar='REPO', type=str,
112 | default='astropy', help='The name of the repository '
113 | 'of the issue and code. must have the same name for '
114 | 'both target and source (default: astropy)')
115 | parser.add_argument('--sourceuser', metavar='USER', type=str,
116 | help='The name of the user/organization whose '
117 | 'repo/fork the pull request should pull from')
118 | parser.add_argument('--targetuser', metavar='USER', type=str,
119 | default='astropy', help='The name of the '
120 | 'user/organization the pull request should pull into '
121 | '(default: astropy)')
122 | parser.add_argument('--targbranch', metavar='BRANCH', type=str,
123 | default='master', help='The branch name the pull '
124 | 'request should be pulled into (default: master)')
125 | parser.add_argument('--baseurl', metavar='URL', type=str,
126 | default=GITHUB_API_BASE_URL, help='The base '
127 | 'URL for github (default: %(default)s)')
128 |
129 | args = parser.parse_args(argv)
130 |
131 | targrepo = args.targetuser + '/' + args.repo
132 |
133 | print('Converting issue', args.issuenum, 'of', targrepo, 'to pull request')
134 | print('Will request to pull branch', args.srcbranch, 'of user\'s',
135 | args.repo, 'repo to branch', args.targbranch, 'of ', targrepo)
136 | issue_to_pr(args.issuenum, args.srcbranch, args.repo, args.sourceuser,
137 | args.targetuser, args.targbranch, args.baseurl)
138 |
139 |
140 | if __name__ == '__main__':
141 | main()
142 |
--------------------------------------------------------------------------------
/update-packages/update_astropy_helpers.py:
--------------------------------------------------------------------------------
1 | # IMPORTANT: The purpose of this script is to update the astropy-helpers
2 | # submodule in all affiliated packages (and a few other packages) that make use
3 | # of the astropy-helpers. This script is not intended to be run by affiliated
4 | # package maintainers, and should only be run with approval of both the
5 | # astropy-helpers and package-template lead maintainers, once an
6 | # astropy-helpers release has been made.
7 |
8 | import os
9 | import re
10 | import sys
11 | import shutil
12 | import tempfile
13 | import subprocess
14 | from distutils.version import LooseVersion
15 |
16 | from github import Github
17 | from common import get_credentials
18 |
19 | try:
20 | HELPERS_TAG = sys.argv[1]
21 | except IndexError:
22 | raise IndexError("Please specify the helpers version as argument")
23 |
24 |
25 | if LooseVersion(HELPERS_TAG) < LooseVersion('v3.0'):
26 | from helpers_2 import repositories
27 | else:
28 | from helpers_3 import repositories
29 |
30 |
31 | BRANCH = 'update-helpers-{0}'.format(HELPERS_TAG)
32 |
33 | GITHUB_API_HOST = 'api.github.com'
34 |
35 | username, password = get_credentials()
36 | gh = Github(username, password)
37 | user = gh.get_user()
38 |
39 | HELPERS_UPDATE_MESSAGE_BODY = re.sub('(\S+)\n', r'\1 ', """
40 | This is an automated update of the astropy-helpers submodule to {0}. This
41 | includes both the update of the astropy-helpers sub-module, and the
42 | ``ah_bootstrap.py`` file, if needed.
43 |
44 |
45 | A full list of changes can be found in the
46 | [changelog](https://github.com/astropy/astropy-helpers/blob/{0}/CHANGES.rst).
47 |
48 |
49 | *This is intended to be helpful, but if you would prefer to manage these
50 | updates yourself, or if you notice any issues with this automated update,
51 | please let {1} know!*
52 |
53 |
54 | Similarly to the core package, the v3.0+ releases of astropy-helpers
55 | require Python 3.5+. We will open automated update PRs with
56 | astropy-helpers v3.2.x only for packages that specifically opt in for it
57 | when they start supporting Python 3.5+ only.
58 | Please let {1} know or add your package to the list in
59 | https://github.com/astropy/astropy-procedures/blob/master/update-packages/helpers_3.py
60 |
61 | """).strip()
62 |
63 |
64 | def run_command(command):
65 | print('-' * 72)
66 | print("Running '{0}'".format(command))
67 | ret = subprocess.call(command, shell=True)
68 | if ret != 0:
69 | raise Exception("Command '{0}' failed".format(command))
70 |
71 |
72 | def ensure_fork_exists(repo):
73 | if repo.owner.login != user.login:
74 | return user.create_fork(repo)
75 | else:
76 | return repo
77 |
78 |
79 | def open_pull_request(fork, repo):
80 |
81 | # Set up temporary directory
82 | tmpdir = tempfile.mkdtemp()
83 | os.chdir(tmpdir)
84 |
85 | # Clone the repository
86 | run_command('git clone {0}'.format(fork.ssh_url))
87 | os.chdir(repo.name)
88 |
89 | # Make sure the branch doesn't already exist
90 | try:
91 | run_command('git checkout origin/{0}'.format(BRANCH))
92 | except:
93 | pass
94 | else:
95 | print("Branch {0} already exists".format(BRANCH))
96 | return
97 |
98 | # Update to the latest upstream master
99 | print("Updating to the latest upstream master.")
100 | run_command('git remote add upstream {0}'.format(repo.clone_url))
101 | run_command('git fetch upstream master')
102 | run_command('git checkout upstream/master')
103 | run_command('git checkout -b {0}'.format(BRANCH))
104 |
105 | # Initialize submodule
106 | print("Initializing submodule.")
107 | run_command('git submodule init')
108 | run_command('git submodule update')
109 |
110 | # Check that the repo uses astropy-helpers
111 | if not os.path.exists('astropy_helpers'):
112 | print("Repository {0} does not use astropy-helpers".format(repo.name))
113 | return
114 |
115 | # Update the helpers
116 | os.chdir('astropy_helpers')
117 | rev_prev = subprocess.check_output('git rev-list HEAD', shell=True).splitlines()
118 | run_command('git fetch origin')
119 |
120 | run_command('git checkout {0}'.format(HELPERS_TAG))
121 | rev_new = subprocess.check_output('git rev-list HEAD', shell=True).splitlines()
122 | if len(rev_new) <= len(rev_prev):
123 | print("Repository {0} already has an up-to-date astropy-helpers".format(repo.name))
124 | return
125 | os.chdir('..')
126 | shutil.copy('astropy_helpers/ah_bootstrap.py', 'ah_bootstrap.py')
127 |
128 | run_command('git add astropy_helpers')
129 | run_command('git add ah_bootstrap.py')
130 | if os.path.exists('ez_setup.py'):
131 | run_command('git rm ez_setup.py')
132 | run_command('git commit -m "Updated astropy-helpers to {0}"'.format(HELPERS_TAG))
133 |
134 | run_command('git push origin {0}'.format(BRANCH))
135 |
136 | print(tmpdir)
137 |
138 | report_user = '@astrofrog'
139 |
140 | if username != 'astrofrog':
141 | report_user = '@{} or @astrofrog'.format(username)
142 |
143 | repo.create_pull(title='Update astropy-helpers to {0}'.format(HELPERS_TAG),
144 | body=HELPERS_UPDATE_MESSAGE_BODY.format(HELPERS_TAG, report_user),
145 | base='master',
146 | head='{0}:{1}'.format(fork.owner.login, BRANCH))
147 |
148 |
149 | START_DIR = os.path.abspath('.')
150 |
151 | for owner, repository in repositories:
152 | print("\n########################")
153 | print("Starting package: {}/{}".format(owner, repository))
154 | print("########################\n")
155 |
156 |
157 | repo = gh.get_repo("{0}/{1}".format(owner, repository))
158 |
159 | fork = ensure_fork_exists(repo)
160 | try:
161 | open_pull_request(fork, repo)
162 | except:
163 | pass
164 | finally:
165 | os.chdir(START_DIR)
166 |
--------------------------------------------------------------------------------
/pr_consistency/README.md:
--------------------------------------------------------------------------------
1 | About
2 | =====
3 |
4 | Scripts
5 | -------
6 |
7 | The scripts in this directory can be used to check for consistency between pull
8 | request milestones, changelog versions, and which branches pull requests appear
9 | in. Since some of the steps take a while to run, they are split into multiple
10 | scripts:
11 |
12 | * ``1.get_merged_prs.py``
13 | * ``2.find_pr_branches.py``
14 | * ``3.find_pr_changelog_section.py``
15 | * ``4.check_consistency.py``
16 |
17 | The first three scripts should be run sequentially.
18 |
19 | The first script requires authentication for GitHub, for which you can either
20 | use a ``.netrc file``, or you will be prompted for your login details.
21 |
22 | These three scripts will produce JSON files which summarize
23 | the required information.
24 |
25 | Once this is done, you can then run ``4.check_consistency.py`` to actually run
26 | all the consistency checks. Note that this script has a ``SHOW_VALID`` option.
27 | If set to `False`, this shows only pull requests for which there are issues.
28 |
29 | Example
30 | -------
31 |
32 | The following shows an example of a typical session. We start off by updating
33 | the JSON file so that it is up-to-date with the pull requests merged into
34 | Astropy:
35 |
36 | $ python 1.get_merged_prs.py
37 | Using the following GitHub credentials from ~/.netrc: username/********
38 | Use these credentials (if not you will be prompted for new credentials)? [Y/n] Y
39 | Fetching new entry for pull request #5008
40 | Fetching new entry for pull request #5010
41 | Fetching new entry for pull request #5012
42 |
43 | We now run the second script to find out for all pull requests which
44 | maintenance branches they are included in. Note that this includes some
45 | preliminary information about issues found at this stage (but these can't
46 | typically be fixed since it would mean changing the history):
47 |
48 | $ python 2.find_pr_branches.py
49 | Cloning git://github.com/astropy/astropy.git
50 | Cloning into 'astropy'...
51 | remote: Counting objects: 103101, done.
52 | remote: Compressing objects: 100% (13/13), done.
53 | remote: Total 103101 (delta 2), reused 0 (delta 0), pack-reused 103088
54 | Receiving objects: 100% (103101/103101), 46.26 MiB | 1.71 MiB/s, done.
55 | Resolving deltas: 100% (78181/78181), done.
56 | Checking connectivity... done.
57 | Switching to branch v0.1.x
58 | HEAD is now at b97729f Merge pull request #5008 from MSeifert04/docfix
59 | Branch v0.1.x set up to track remote branch v0.1.x from origin.
60 | Switched to a new branch 'v0.1.x'
61 | Switching to branch v0.2.x
62 | HEAD is now at 3f2b9eb Merge pull request #291 from mdboom/logging/no-astropy-dir
63 | Branch v0.2.x set up to track remote branch v0.2.x from origin.
64 | Switched to a new branch 'v0.2.x'
65 | Switching to branch v0.3.x
66 | HEAD is now at 6d60c3e Back to development: 0.2.6
67 | Branch v0.3.x set up to track remote branch v0.3.x from origin.
68 | Switched to a new branch 'v0.3.x'
69 | Pull request 663 appears 3 times in branch v0.3.x
70 | Switching to branch v0.4.x
71 | HEAD is now at 4202159 Back to development: 0.3.3
72 | Branch v0.4.x set up to track remote branch v0.4.x from origin.
73 | Switched to a new branch 'v0.4.x'
74 | Pull request 663 appears 3 times in branch v0.4.x
75 | Switching to branch v1.0.x
76 | HEAD is now at 65444af Back to development: 0.4.7
77 | Branch v1.0.x set up to track remote branch v1.0.x from origin.
78 | Switched to a new branch 'v1.0.x'
79 | Pull request 3766 appears 2 times in branch v1.0.x
80 | Pull request 663 appears 3 times in branch v1.0.x
81 | Pull request 4228 appears 2 times in branch v1.0.x
82 | Switching to branch v1.1.x
83 | HEAD is now at 0448e5b Merge pull request #3810 from taldcroft/column-scalar
84 | Branch v1.1.x set up to track remote branch v1.1.x from origin.
85 | Switched to a new branch 'v1.1.x'
86 | Pull request 663 appears 3 times in branch v1.1.x
87 | Switching to branch v1.2.x
88 | HEAD is now at 955ea32 Merge pull request #4893 from bsipocz/cleanup_changing_log.warn
89 | Branch v1.2.x set up to track remote branch v1.2.x from origin.
90 | Switched to a new branch 'v1.2.x'
91 | Pull request 663 appears 3 times in branch v1.2.x
92 |
93 | We then check which sections of the changelog pull requests appear in:
94 |
95 | $ python 3.find_pr_changelog_section.py
96 |
97 | Note that we now have three JSON files with information from the above three
98 | scripts:
99 |
100 | $ ls *.json
101 | merged_pull_requests.json
102 | pull_requests_branches.json
103 | pull_requests_changelog_sections.json
104 |
105 | Finally, we run the script to check the consistency of all the information:
106 |
107 | $ python 4.check_consistency.py
108 | #4060 (Milestone: v1.0.10)
109 | - Milestone is v1.0.10 but change log section is v1.0.5
110 | - Pull request was included in branch v1.0.x
111 | - Pull request was included in branch v1.1.x
112 | - Pull request was included in branch v1.2.x
113 | #4928 (Milestone: v1.2.0)
114 | - Labelled as no-changelog-entry-needed and not in changelog
115 | - Pull request was not included in branch v1.2.x
116 | #4972 (Milestone: v1.0.10)
117 | - Labelled as no-changelog-entry-needed and not in changelog
118 | - Pull request was not included in branch v1.0.x
119 | - Pull request was not included in branch v1.1.x (but too late to fix)
120 | - Pull request was not included in branch v1.2.x
121 | #4984 (Milestone: v1.2.0)
122 | - Labelled as no-changelog-entry-needed and not in changelog
123 | - Pull request was not included in branch v1.2.x
124 | #4995 (Milestone: v1.2.0)
125 | - Labelled as no-changelog-entry-needed and not in changelog
126 | - Pull request was not included in branch v1.2.x
127 | #4997 (Milestone: v1.2.0)
128 | - Labelled as no-changelog-entry-needed and not in changelog
129 | - Pull request was not included in branch v1.2.x
130 | #5008 (Milestone: v1.0.10)
131 | - Labelled as no-changelog-entry-needed and not in changelog
132 | - Pull request was not included in branch v1.0.x
133 | - Pull request was not included in branch v1.1.x (but too late to fix)
134 | - Pull request was not included in branch v1.2.x
135 |
136 | If you want to see all results even pull requests that are valid, you can change
137 | ``SHOW_VALID`` to ``True`` in ``4.check_consistency.py ``.
138 |
139 | Updating for New Branches
140 | -------------------------
141 |
142 | When you add a new branch to these scripts, it is important to add the new
143 | branch name in *both* of these:
144 |
145 | * ``2.find_pr_branches.py``
146 | * ``4.check_consistency.py``
147 |
148 | Additionally, in ``4.check_consistency.py`` you'll need to add the branch to
149 | the ``BRANCH_CLOSED`` dictionary.
--------------------------------------------------------------------------------
/discontinued_usage/gh_issuereport.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 |
4 | """
5 | This script will use PyPI to identify a release of a package, and then search
6 | through github to get a count of all of the issues and PRs closed/merged since
7 | that release.
8 |
9 | Usage:
10 | python gh_issuereport.py astropy/astropy astropy/0.3
11 |
12 | Note that it will prompt you for yout github username/password. This isn't
13 | necessary if you have less than 6000 combined issues/PRs, but if you have more
14 | than that (or run the script multiple times in an hour without caching), you
15 | might hit github's 60 api calls per hour limit (which increases to 5000 if you
16 | log in).
17 |
18 | Also note that running this will by default cache the PRs/issues in "prs.json"
19 | and "issues.json". Give it the "-n" option to not do that.
20 |
21 | Requires the requests package (https://pypi.python.org/pypi/requests/).
22 |
23 | """
24 |
25 | import argparse
26 | import os
27 | import json
28 | import datetime
29 |
30 | from common import get_credentials
31 |
32 | import requests
33 | from six import moves
34 |
35 | GH_API_BASE_URL = 'https://api.github.com'
36 | ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
37 |
38 |
39 | def paginate_list_request(req, verbose=False, auth=None):
40 | elems = []
41 | currreq = req
42 | i = 1
43 |
44 | while 'next' in currreq.links:
45 | elems.extend(currreq.json())
46 |
47 | i += 1
48 | if verbose:
49 | print('Doing request', i, 'of', currreq.links['last']['url'].split('page=')[-1])
50 | currreq = requests.get(currreq.links['next']['url'], auth=auth)
51 |
52 | elems.extend(currreq.json())
53 | return elems
54 |
55 |
56 | def count_issues_since(dt, repo, auth=None, verbose=True, cacheto=None):
57 | if cacheto and os.path.exists(cacheto):
58 | with open(cacheto) as f:
59 | isslst = json.load(f)
60 | else:
61 | url = GH_API_BASE_URL + '/repos/' + repo + '/issues?per_page=100&state=all'
62 |
63 | req = requests.get(url, auth=auth)
64 | if not req.ok:
65 | msg = 'Failed to access github API for repo using url {}. {}: {}: {}'
66 | raise requests.HTTPError(msg.format(url, req.status_code, req.reason, req.text))
67 | isslst = paginate_list_request(req, verbose, auth=auth)
68 | if cacheto:
69 | with open(cacheto, 'w') as f:
70 | json.dump(isslst, f)
71 |
72 | nopened = nclosed = 0
73 |
74 | for entry in isslst:
75 | createddt = datetime.datetime.strptime(entry['created_at'], ISO_FORMAT)
76 | if createddt > dt:
77 | nopened += 1
78 |
79 | if entry['closed_at']:
80 | closeddt = datetime.datetime.strptime(entry['closed_at'], ISO_FORMAT)
81 | if closeddt > dt:
82 | nclosed += 1
83 |
84 | return {'opened': nopened, 'closed': nclosed}
85 |
86 |
87 | def count_prs_since(dt, repo, auth=None, verbose=True, cacheto=None):
88 | if cacheto and os.path.exists(cacheto):
89 | with open(cacheto) as f:
90 | prlst = json.load(f)
91 | else:
92 | url = GH_API_BASE_URL + '/repos/' + repo + '/pulls?per_page=100&state=all'
93 |
94 | req = requests.get(url, auth=auth)
95 | prlst = paginate_list_request(req, verbose, auth=auth)
96 | if cacheto:
97 | with open(cacheto, 'w') as f:
98 | json.dump(prlst, f)
99 |
100 |
101 | nopened = nclosed = 0
102 |
103 | usersopened = []
104 | usersclosed = []
105 |
106 | for entry in prlst:
107 | createddt = datetime.datetime.strptime(entry['created_at'], ISO_FORMAT)
108 | if createddt > dt:
109 | nopened += 1
110 | user = entry['user']
111 | if user is not None:
112 | usersopened.append(user['id'])
113 |
114 | if entry['merged_at']:
115 | closeddt = datetime.datetime.strptime(entry['merged_at'], ISO_FORMAT)
116 | if closeddt > dt:
117 | nclosed += 1
118 | user = entry['user']
119 | if user is not None:
120 | usersclosed.append(user['id'])
121 |
122 | return {'opened': nopened, 'merged': nclosed,
123 | 'usersopened': len(set(usersopened)),
124 | 'usersmerged': len(set(usersclosed))}
125 |
126 |
127 | def get_datetime_of_pypi_version(pkg, version):
128 | from xml.dom import minidom
129 |
130 | url = 'http://pypi.python.org/pypi/{0}/{1}'.format(pkg, version)
131 |
132 | dom = minidom.parseString(requests.get(url).content)
133 |
134 | table = dom.getElementsByTagName('table')[0]
135 | t = table.getElementsByTagName('tr')[1].getElementsByTagName('td')[-2].firstChild
136 | if t.nodeType != t.TEXT_NODE:
137 | raise ValueError("pypi page for {0}/{1} ddoesn't seem to have a date".format(pkg, version))
138 | else:
139 | datestr = t.data
140 |
141 | return datetime.datetime.strptime(datestr, "%Y-%m-%d")
142 |
143 |
144 | def main(argv=None):
145 | parser = argparse.ArgumentParser(description='Print out issue stats since a particular version of a repo.')
146 | parser.add_argument('repo', help='the github repo to use', metavar='/')
147 | parser.add_argument('package', help='the package/version to lookup on pypi '
148 | 'or "None" to skip the lookup',
149 | metavar='/')
150 |
151 | parser.add_argument('-q', '--quiet', help='hide informational messages',
152 | dest='verbose', action='store_false')
153 | parser.add_argument('-n', '--no-cache', help="don't cache the downloaded "
154 | "issue/PR info (and don't read "
155 | "the cached versions)",
156 | dest='cache', action='store_false')
157 |
158 | args = parser.parse_args(argv)
159 | if args.package.lower() == 'none':
160 | # probably nothing on github was created before the year 1900...
161 | pkgdt = datetime.datetime(1900, 1, 1)
162 | if args.verbose:
163 | print('Not looking up a PyPI package')
164 | else:
165 | pkgdt = get_datetime_of_pypi_version(*args.package.split('/'))
166 | if args.verbose:
167 | print('Found PyPI entry for', args.package, ':', pkgdt)
168 |
169 | auth = get_credentials()
170 |
171 | if args.cache:
172 | icache = 'issues.json'
173 | prcache = 'prs.json'
174 | else:
175 | icache = prcache = None
176 |
177 | if args.verbose:
178 | print('Counting issues')
179 | icnt = count_issues_since(pkgdt, args.repo, auth=auth, verbose=args.verbose, cacheto=icache)
180 | if args.verbose:
181 | print('Counting PRs')
182 | prcnt = count_prs_since(pkgdt, args.repo, auth=auth, verbose=args.verbose, cacheto=prcache)
183 |
184 | print(icnt['opened'], 'issues opened since', args.package, 'and', icnt['closed'], 'issues closed')
185 | print(prcnt['opened'], 'PRs opened since', args.package, 'and', prcnt['merged'], 'PRs merged')
186 | print(prcnt['usersopened'], 'unique users opened PRs, and', prcnt['usersmerged'], 'of them got it merged')
187 |
188 |
189 | if __name__ == '__main__':
190 | main()
191 |
--------------------------------------------------------------------------------
/documents/affiliated_package_review_guidelines.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | This version of this documented is deprecated. The canonical copy can be found [in the astropy project-wide policies repo](https://github.com/astropy/project/blob/master/affiliated/affiliated_package_review_guidelines.md).
4 |
5 | # Reviewing affiliated packages
6 |
7 | This document describes the set of review criteria for packages applying to
8 | become Astropy-affiliated, and is intended for reviewers.
9 |
10 | If you are reading this because you have accepted to review an affiliated
11 | package submission, thank you for taking the time to do this!
12 |
13 | Note that unlike for a paper where the reviews from the referee are passed on to
14 | the authors, one of the coordinators will also review the package and will
15 | create a combined report, so the report you write may not be seen by the
16 | authors as-is. Reports should be emailed privately back to the coordinator that
17 | contacted you.
18 |
19 | Reviewing a package involves assessing how well the package does in several
20 | areas, which we outline below. As a reviewer it is is expected that you review
21 | on these criteria, and it is sufficient to *solely* use these criteria .
22 | However, feel free to bring up any other aspect which you think is important.
23 | For the categories below, you can let us know which of the 'traffic light'
24 | levels you think the package should be rated as, in addition to providing
25 | comments in cases where things aren't perfect.
26 |
27 | In general we use the following color-coding, which also determines if a package
28 | is accepted after its first review:
29 |
30 |
31 |
32 |  |
33 | Affiliated packages can only be accepted into the list if there are no red scores. Existing affiliated packages that have one category that drops down to red don’t get automatically kicked out, but they should resolve this, otherwise the package will be de-listed. |
34 |
35 |
36 |  |
37 | Having orange scores is fine, but is both a warning to the user that not all is perfect about the package, and an incentive for developers to improve and reach green. |
38 |
39 |
40 |  |
41 | This is the standard we want all packages to aim for. Packages that have all-green scores may be featured in a separate table for well-maintained and supported packages. |
42 |
43 |
44 |
45 | The document also includes ``monospaced keywords`` for the categories and levels. These are the keywords and values to be used in the [registry.json](http://www.astropy.org/affiliated/registry.json) file that is the canonical source for affiliated package information.
46 |
47 | The categories in which we assess the package are the following:
48 |
49 | * Functionality (``'functionality'``)
50 | * Integration with Astropy ecosystem (``'ecointegration'``)
51 | * Documentation (``'documentation'``)
52 | * Testing (``'testing'``)
53 | * Development status (``'devstatus'``)
54 | * Python 3 compatibility (``'python3'``)
55 |
56 | ### Functionality ('functionality')
57 |
58 | We first need to make sure that the scope of the package is relevant for the affiliated package system. The scopes are:
59 |
60 |
61 |
62 |  |
63 | Not useful for astronomers, or specific to one project/collaboration. |
64 |
65 |
66 |  |
67 | Useful to astronomers working in a very specific domain/field, or with a specific telescope instrument and usable not just by a single collaboration but any astronomers within that domain. Packages such as sncosmo fall into this category. |
68 |
69 |
70 |  |
71 | Package that is useful for astronomers across more than a single field/instrument/telescope. Packages such as astroquery or astroplan fall into this category. |
72 |
73 |
74 |
75 | Note that general is not necessary better than specific, it’s just a way to make sure we can present these separately.
76 |
77 | ### Integration with Astropy ecosystem ('ecointegration')
78 |
79 | Next up, we need to check how well the package fits in to the existing Astropy
80 | ecosystem - does it make use of existing functionality, or does it duplicate it?
81 |
82 |
83 |
84 |  |
85 | Does not use Astropy or other affiliated packages anywhere where it should be possible, and/or uses other libraries instead. |
86 |
87 |
88 |  |
89 | Makes an effort to use Astropy or other affiliated packages in places, but still has other places where this could be done but isn’t |
90 |
91 |
92 |  |
93 | Uses Astropy or other affiliated packages wherever possible. |
94 |
95 |
96 |
97 | ### Documentation ('documentation')
98 |
99 | No code is complete without documentation! Take a look at the documentation (if
100 | it exists) and see how the package fares:
101 |
102 |
103 |
104 |  |
105 | No documentation or some documentation, but very bare bones/minimal and incomplete or incorrect in a number of places. |
106 |
107 |
108 |  |
109 | Reasonable documentation (which could be a very well written README), installation instructions and at least one usage example, but some parts missing. |
110 |
111 |
112 |  |
113 | Extensive documentation, including at least: motivation/scope of package, installation instructions, usage examples, API documentation. In terms of infrastructure, the documentation should be automatically built on readthedocs.org. If appropriate, one or more tutorials should be included in the Astropy tutorials at http://tutorials.astropy.org. |
114 |
115 |
116 |
117 | ### Testing ('testing')
118 |
119 | In our terminology, “tests” refer to those that can be run in an automated way,
120 | and we do not consider examples that need to be run and/or checked manually to
121 | be acceptable as the primary way of satisfying “tests”
122 |
123 |
124 |
125 |  |
126 | No tests or tests that are not trivial to run or don’t use a standard testing framework, or low test coverage (no exact threshold for coverage since this is not always easy to measure, but in this category most of the code is not covered). |
127 |
128 |
129 |  |
130 | A reasonable fraction of the code is covered by tests, but still some parts of the code that are missing tests. To be in this category, packages should use a standard framework (pytest, nose, etc.) and be runnable with a single command. |
131 |
132 |
133 |  |
134 | Test coverage is very high (for example 90% or more), tests use a standard framework (pytest, nose, etc.) and are easy to run and continuous integration is used to ensure package stability over time. |
135 |
136 |
137 |
138 | Test coverage can be tricky to measure, so this will be carefully assessed for
139 | each package. The main idea is to determine whether it is low, medium or high
140 | compared to what one might realistically achieve.
141 |
142 | ### Development status ('devstatus')
143 |
144 |
145 |
146 |  |
147 | No active development/maintainers, even if stable releases exist, if package is not or no longer fully functional. |
148 |
149 |
150 |  |
151 | Stable releases exist, but still under heavy development (so API changes can be frequent). |
152 |
153 |
154 |  |
155 | Stable releases exist and there are no active developers/maintainers but package remains mostly functional. |
156 |
157 |
158 |  |
159 | Stable releases exist but the maintainers only make occasional comments/commits (and package is not in excellent condition, because otherwise it’s fine to have a completely stable package with little activity if it can be considered 'finished') |
160 |
161 |  |
162 | Package has stable releases, and package is actively developed (as needed). A metric for active development is whether most recently-opened issues have some kind of reply from maintainers. |
163 |
164 |
165 |
166 | ### Python 3 compatibility ('python3')
167 |
168 |
169 |
170 |  |
171 | Nothing is currently 'unacceptable'. Starting 1 January 2020, 'Not compatible with Python 3' will be red. |
172 |
173 |
174 |  |
175 | Not compatible with Python 3 |
176 |
177 |
178 |  |
179 | Compatible with Python 3 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/visualizations_demographics/astropy_status_plots.py:
--------------------------------------------------------------------------------
1 | """
2 | This module has functions to generate various information plots that can be
3 | extracted from a git repository like how the number of commits and committers
4 | grow over time.
5 |
6 | Requires that you have a file that can be generatedwith:
7 | git log --numstat --use-mailmap --format=format:"COMMIT,%H,%at,%aN"
8 | """
9 |
10 | import os
11 | import numpy as np
12 | from matplotlib import pyplot as plt
13 |
14 | def generate_commit_stats_file(fn='gitlogstats', overwrite=False, dirtorunin=None):
15 | """
16 | Running this will generate a file in the current directory that stores the
17 | statistics to make generating lots of plots easier. Delete the file or
18 | set `overwrite` to True to always re-generate the statistics.
19 | """
20 | if os.path.isfile(fn) and not overwrite:
21 | with open(fn) as f:
22 | return f.read()
23 | else:
24 | import subprocess
25 |
26 | cmd = 'git log --numstat --use-mailmap --format=format:"COMMIT,%H,%at,%aN"'.split()
27 | output = subprocess.check_output(cmd, cwd=dirtorunin)
28 | with open(fn, 'wb') as f:
29 | f.write(output)
30 | return output.decode()
31 |
32 |
33 | def parse_git_log(fn='gitlogstats', recentfirst=False, cumlines=False):
34 | """
35 | Returns (authors, datetimes, deltalines) arrays
36 | """
37 | from datetime import datetime
38 |
39 | authors = []
40 | deltalines = []
41 | datetimes = []
42 |
43 | commitstrs = generate_commit_stats_file(fn).split('COMMIT,')[1:]
44 |
45 | for cs in commitstrs:
46 | lns = cs.strip().split('\n')
47 | chash, timestamp, author = lns.pop(0).split(',')
48 | timestamp = int(timestamp)
49 |
50 | authors.append(author)
51 | datetimes.append(datetime.utcfromtimestamp(timestamp))
52 |
53 | dellns = 0
54 | for l in lns:
55 | if len(l.split('\t')) != 3:
56 | #print('skipping "{0}"'.format(l))
57 | continue
58 | add, sub, filename = l.split('\t')
59 | dellns += int(add.replace('-', '0')) - int(sub.replace('-', '0'))
60 | deltalines.append(dellns)
61 |
62 | authors, datetimes, deltalines = [np.array(l) for l in (authors, datetimes, deltalines)]
63 |
64 | sorti = np.argsort(datetimes)
65 | revsorti = np.argsort(sorti)
66 |
67 | if cumlines:
68 | deltalines = np.cumsum(deltalines[sorti])[revsorti]
69 |
70 | if recentfirst:
71 | sorti = sorti[::-1]
72 |
73 | return tuple([a[sorti] for a in (authors, datetimes, deltalines)])
74 |
75 |
76 | def loc_plot(yrlabels=None):
77 | from datetime import datetime
78 |
79 | authors, datetimes, nlines = parse_git_log(cumlines=True, recentfirst=False)
80 |
81 | plt.plot(datetimes, nlines, lw=2)
82 |
83 | if yrlabels is None:
84 | yrlabels = np.arange((datetime.today().year - 2011) + 1) + 2011
85 |
86 | plt.xticks([datetime(yr, 1, 1) for yr in yrlabels], yrlabels, fontsize=20)
87 | plt.ylabel('Lines of Code', fontsize=30)
88 |
89 | plt.tight_layout()
90 |
91 |
92 | def commits_plot(yrlabels=None, **plotkws):
93 | from datetime import datetime
94 |
95 | authors, datetimes, deltalines = parse_git_log(recentfirst=False)
96 |
97 | plotkws.setdefault('lw', 2)
98 | plt.plot(datetimes, np.arange(len(datetimes)) + 1, **plotkws)
99 |
100 | if yrlabels is None:
101 | yrlabels = np.arange((datetime.today().year - 2011) + 1) + 2011
102 |
103 |
104 | plt.xticks([datetime(yr, 1, 1) for yr in yrlabels], yrlabels, fontsize=20)
105 | plt.ylabel('Number of Commits', fontsize=30)
106 |
107 | plt.tight_layout()
108 |
109 |
110 | def get_first_commit_map():
111 | authors, datetimes, deltalines = parse_git_log(recentfirst=False)
112 |
113 | firstcommit = {}
114 | for au, t in zip(authors, datetimes):
115 | if au not in firstcommit or firstcommit[au] > t:
116 | firstcommit[au] = t
117 |
118 | return firstcommit
119 |
120 |
121 | def commiters_plot(yrlabels=None, **plotkws):
122 | from datetime import datetime
123 |
124 | firstcommit = get_first_commit_map()
125 |
126 | dts = np.sort(list(firstcommit.values()))
127 |
128 | plotkws.setdefault('lw', 2)
129 | plt.plot(dts, np.arange(len(dts)) + 1, **plotkws)
130 |
131 | if yrlabels is None:
132 | yrlabels = np.arange((datetime.today().year - 2011) + 1) + 2011
133 |
134 | plt.xticks([datetime(yr, 1, 1) for yr in yrlabels], yrlabels, fontsize=20)
135 |
136 | plt.ylim(0, len(dts)+1)
137 | plt.xlim(dts[0], dts[-1])
138 |
139 | ytks = [y for y in plt.yticks()[0] if y0]
91 | values[lang] = values[lang][values[lang]>0]
92 |
93 | if 'total' not in years:
94 | years['total'], values['total'] = get_total()
95 |
96 |
97 | if 'iraf_plus_pyraf' not in locals():
98 | iraf_plus_pyraf = 'Done'
99 | for ii,yr in enumerate(years['IRAF']):
100 | inds = years['pyraf'] == yr
101 | if any(inds):
102 | values['IRAF'][ii] += values['pyraf'][inds]
103 |
104 | if 'astropy2013' not in years:
105 | # astropy 2013 paper
106 | astropy2013ppr = get_citation_counts_for_paper('2013A&A...558A..33A')
107 | years['astropy2013'] = np.array(astropy2013ppr[0])
108 | values['astropy2013'] = np.array(astropy2013ppr[1])
109 |
110 | if 'astropy2018' not in years:
111 | # astropy 2018 paper
112 | astropy2018ppr = get_citation_counts_for_paper('2018AJ....156..123A')
113 | years['astropy2018'] = np.array(astropy2018ppr[0])
114 | values['astropy2018'] = np.array(astropy2018ppr[1])
115 |
116 |
117 | if 'yt' not in years:
118 | # yt 2011 paper
119 | ytppr = get_citation_counts_for_paper('2011ApJS..192....9T')
120 | years['yt'] = np.array(ytppr[0])
121 | values['yt'] = np.array(ytppr[1])
122 |
123 |
124 | plt.rc('font', family='Source Sans Pro')
125 |
126 | fig = plt.figure(num=1, figsize=(10,6))
127 | fig.clf()
128 | ax = fig.add_subplot(1,1,1)
129 |
130 | ax.set_title("Full text mentions of programming languages in Astronomy papers", size=10)
131 | ax.plot(years['Python'], values['Python'], '.-', label='Python')
132 | ax.plot(years['IDL'], values['IDL'], '.-', label='IDL')
133 | ax.plot(years['Fortran'], values['Fortran'], '.-', label='Fortran')
134 | ax.plot(years['Matlab'], values['Matlab'], '.-', label='Matlab')
135 | ax.plot(years['Astropy'], values['Astropy'], '.-', label='Astropy')
136 |
137 | ax.legend(fontsize=8, loc=2)
138 | # can't show this year b/c it isn't normalized
139 | ax.set_xlim(1970, thisyear)
140 | ax.xaxis.get_major_formatter().set_useOffset(False)
141 | ax.set_ylabel("Number of papers")
142 | ax.set_xlabel("Year")
143 | plt.savefig('hockey_stick_graph.png', dpi=150)
144 |
145 |
146 |
147 | fig = plt.figure(num=2, figsize=(10,6))
148 | fig.clf()
149 | ax = fig.add_subplot(1,1,1)
150 |
151 | def get_ratio(kw1, kw2, return_years=False):
152 | yr1, val1 = years[kw1], values[kw1]
153 | yr2, val2 = years[kw2], values[kw2]
154 | ryears = []
155 | ratio = []
156 | for yr, num in zip(yr1, val1):
157 | try:
158 | ind = list(yr2).index(yr)
159 | den = val2[ind]
160 | ratio.append(num/den)
161 | ryears.append(yr)
162 | except ValueError as ex:
163 | print(ex)
164 |
165 | #assert len(yr1) == len(ratio)
166 |
167 | if return_years:
168 | return np.array(ryears), np.array(ratio)
169 | else:
170 | return np.array(ratio)
171 |
172 | ax.set_title("Fraction of papers mentioning each language", size=10)
173 | ax.plot(years['Python'], get_ratio('Python', 'total'), '.-', label='Python')
174 | ax.plot(years['IDL'], get_ratio('IDL', 'total'), '.-', label='IDL')
175 | ax.plot(years['Fortran'], get_ratio('Fortran', 'total'), '.-', label='Fortran')
176 | ax.plot(years['Matlab'], get_ratio('Matlab', 'total'), '.-', label='Matlab')
177 | ax.plot(years['Astropy'], get_ratio('Astropy', 'total'), '.-', label='Astropy')
178 |
179 | ax.legend(fontsize=8, loc=2)
180 | ax.set_xlim(1970, thisyear)
181 | ax.xaxis.get_major_formatter().set_useOffset(False)
182 | ax.set_ylabel("Fraction of all astronomy papers")
183 | ax.set_xlabel("Year")
184 | plt.savefig('hockey_stick_graph_normalized.png', dpi=150)
185 |
186 | ax.plot(years['IRAF'], get_ratio('IRAF', 'total'), '.-', label='IRAF+pyraf')
187 | #ax.plot(years['pyraf'], get_ratio('pyraf', 'total'), '.-', label='pyraf')
188 | ax.plot(years['CASA'], get_ratio('CASA', 'total'), '.-', label='CASA')
189 | ax.plot(years['AIPS'], get_ratio('AIPS', 'total'), '.-', label='AIPS')
190 | ax.plot(years['gildas'], get_ratio('gildas', 'total'), '.-', label='gildas')
191 | ax.plot(years['virtual observatory'], get_ratio('virtual observatory', 'total'), '.-', label='virtual observatory')
192 | ax.legend(fontsize=8, loc=2)
193 | plt.savefig('hockey_stick_graph_normalized_all.png', dpi=150)
194 |
195 | fig = plt.figure(num=3, figsize=(10,6))
196 | fig.clf()
197 | ax = fig.add_subplot(1,1,1)
198 |
199 | ax.set_title("Full text mentions of programming languages in Astronomy papers", size=10)
200 | ax.plot(years['Python'], values['Python'], '.-', label='Python')
201 | ax.plot(years['IDL'], values['IDL'], '.-', label='IDL')
202 | ax.plot(years['Fortran'], values['Fortran'], '.-', label='Fortran')
203 | ax.plot(years['Matlab'], values['Matlab'], '.-', label='Matlab')
204 | ax.plot(years['Astropy'], values['Astropy'], '.-', label='Astropy')
205 | ax.plot(years['CASA'], values['CASA'], '.-', label='CASA')
206 | ax.plot(years['AIPS'], values['AIPS'], '.-', label='AIPS')
207 | #ax.plot(years['astropy affiliated'], values['astropy affiliated'], '.-', label='astropy affiliated', alpha=0.5)
208 | #ax.plot(years['astroquery'], values['astroquery'], '.-', label='astroquery', alpha=0.5)
209 | ax.plot(years['IRAF'], values['IRAF'], '.-', label='IRAF+pyraf', alpha=0.5)
210 | #ax.plot(years['pyraf'], values['pyraf'], '.-', label='pyraf', alpha=0.5)
211 | #ax.plot(years['astropy2013'], values['astropy2013'], '.-', label='astropy2013', alpha=0.5)
212 | #ax.plot(years['astropy2018'], values['astropy2018'], '.-', label='astropy2018', alpha=0.5)
213 | ax.plot(years['yt'], values['yt'], '.--', label='yt', alpha=0.5)
214 | ax.plot(years['gildas'], values['gildas'], '.--', label='gildas', alpha=0.5)
215 | ax.plot(years['virtual observatory'], values['virtual observatory'], '.--', label='virtual observatory')
216 | #ax.plot(years['karma'], values['karma'], '.--', label='karma', alpha=0.5)
217 | ax.plot(years['starlink'], values['starlink'], '.--', label='starlink', alpha=0.5)
218 | #ax.plot(years['gipsy'], values['gipsy'], '.--', label='gipsy', alpha=0.5)
219 | #ax.plot(years['sunpy'], values['sunpy'], '.-', label='sunpy', alpha=0.5)
220 |
221 | ax.legend(fontsize=8, loc=2)
222 | # can't show this year b/c it isn't normalized
223 | ax.set_xlim(1970, thisyear)
224 | ax.xaxis.get_major_formatter().set_useOffset(False)
225 | ax.set_ylabel("Number of papers")
226 | ax.set_xlabel("Year")
227 | plt.savefig('hockey_stick_graph_withCASAandAIPS.png', dpi=150)
228 | ax.set_xlim(2000, thisyear)
229 | plt.savefig('hockey_stick_graph_withCASAandAIPS_since2000.png', dpi=150)
230 | ax.set_xlim(2008, thisyear)
231 | plt.savefig('hockey_stick_graph_withCASAandAIPS_since2008.png', dpi=150)
232 |
233 |
234 | fig = plt.figure(num=4, figsize=(10,6))
235 | fig.clf()
236 | ax = fig.add_subplot(1,1,1)
237 |
238 | ax.set_title("Fraction of python papers mentioning astropy", size=10)
239 | fracyrs, astropy_fraction = get_ratio('Astropy', 'Python', return_years=True)
240 | ax.plot(fracyrs[fracyrs>2000], astropy_fraction[fracyrs>2000], '.-',
241 | label='Astropy/Python')
242 | fracyrs2, astropycite_fraction = get_ratio('astropy2013', 'Astropy', return_years=True)
243 | ax.plot(fracyrs2[fracyrs2>2000], astropycite_fraction[fracyrs2>2000], '.-',
244 | label='Astropy 2013/Astropy')
245 | plt.legend(loc='best')
246 |
247 | #ax.legend(fontsize=8, loc=2)
248 | # can't show this year b/c it isn't normalized
249 | ax.set_xlim(1970, thisyear)
250 | #ax.set_ylim(0, astropy_fraction[np.isfinite(astropy_fraction)].max()+0.05)
251 | ax.xaxis.get_major_formatter().set_useOffset(False)
252 | ax.set_ylabel("Fraction of papers")
253 | ax.set_xlabel("Year")
254 | ax.set_xlim(2012, thisyear)
255 | plt.savefig('hockey_stick_graph_astropyfraction_since2012.png', dpi=150)
256 |
257 |
258 | fig = plt.figure(num=5, figsize=(10,6))
259 | fig.clf()
260 | ax = fig.add_subplot(1,1,1)
261 |
262 | ax.set_title("", size=10)
263 |
264 |
265 | ax.plot(years['Astropy'], values['Astropy'], '.-', label='Astropy')
266 | ax.plot(years['github'], values['github'], '.-', label='github')
267 | ax.plot(years['bitbucket'], values['bitbucket'], '.-', label='bitbucket')
268 | ax.plot(years['cvs'], values['cvs'], '.-', label='cvs')
269 | ax.plot(years['svn'], values['svn'], '.-', label='svn')
270 | plt.legend(loc='best')
271 |
272 | #ax.legend(fontsize=8, loc=2)
273 | # can't show this year b/c it isn't normalized
274 | ax.set_xlim(1970, thisyear)
275 | #ax.set_ylim(0, astropy_fraction[np.isfinite(astropy_fraction)].max()+0.05)
276 | ax.xaxis.get_major_formatter().set_useOffset(False)
277 | ax.set_ylabel("Number of papers")
278 | ax.set_xlabel("Year")
279 | ax.set_xlim(2000, thisyear)
280 | plt.savefig('hockey_stick_graph_vcs.png', dpi=150)
281 |
--------------------------------------------------------------------------------
/documents/affiliated_package_review_process.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | This version of this documented is deprecated. The canonical copy can be found [in the astropy project-wide policies repo](https://github.com/astropy/project/blob/master/affiliated/affiliated_package_review_process.md).
4 |
5 |
6 | # Procedure for the proposal, review, and acceptance of Astropy-affiliated packages
7 |
8 | This document describes the procedure for proposing, reviewing, and making a
9 | decision about Astropy-affiliated packages. This is meant as a record of the
10 | procedure and not the first place people should go to for - for instructions
11 | on proposing an affiliated package, see [**Becoming an Affiliated Package**](http://www.astropy.org/affiliated/index.html).
12 |
13 | ## Proposing an affiliated package
14 |
15 | *These instructions are for affiliated package authors*
16 |
17 | Once you believe your package is ready to be reviewed by the
18 | Astropy Project, open a pull request to the
19 | [astropy.github.com](https://github.com/astropy/astropy.github.com)
20 | repository, modifying the ``affiliated/registry.json`` file to add details
21 | about your own package. If you are not comfortable with git, you can do this by using [an editor](https://github.com/astropy/astropy.github.com/edit/master/affiliated/registry.json).
22 | If you get a message saying **You need
23 | to fork this repository to propose changes** then click on the green button
24 | to confirm the forking:
25 |
26 |
27 |
28 |
29 |
30 | Copy the following template entry and fill out the
31 | details:
32 |
33 | {
34 | "name": "Your package name",
35 | "maintainer": "Your name ",
36 | "stable": true,
37 | "home_url": "URL to the home page or documentation",
38 | "repo_url": "URL to the repository",
39 | "pypi_name": "PyPI name if it exists, set to an empty string otherwise",
40 | "description": "A few sentence description of your package",
41 | "review": {
42 | "functionality": "To be filled out by the reviewer",
43 | "ecointegration": "To be filled out by the reviewer",
44 | "documentation": "To be filled out by the reviewer",
45 | "testing": "To be filled out by the reviewer",
46 | "devstatus": "To be filled out by the reviewer",
47 | "python3": "To be filled out by the reviewer",
48 | "last-updated": "To be filled out by the reviewer"
49 | }
50 | },
51 |
52 | then add it at the top of the ``registry.json`` file, starting on line 3,
53 | after:
54 |
55 | {
56 | "packages": [
57 |
58 | Once this is done, under **Propose file change** you can enter a short
59 | description for the commit then click on the green **Propose file change**
60 | button:
61 |
62 |
63 |
64 |
65 |
66 | On the next screen, check the changes you made, and click on the green
67 | **Create pull request** button:
68 |
69 |
70 |
71 |
72 |
73 | Finally, give a title to the pull request such as **Proposed affiliated
74 | package: your package name** and include a short description of the
75 | package, then click on **Create pull request**:
76 |
77 |
78 |
79 |
80 |
81 | Once this is done, send an email to https://groups.google.com/forum/#!forum/astropy-dev
82 | (which is the main developer list) to introduce your package and say that
83 | you would like it to be considered as an affiliated package. Make sure you
84 | include a link to the pull request you opened above. Once you've done this,
85 | you're all set! The next steps are the review, for which the results will be
86 | posted on the pull request and also in a reply to your email.
87 |
88 | ## Reviewing affiliated packages
89 |
90 | *These instructions are for coordinators and reviewers*
91 |
92 | One of the coordination committee members will be in charge of the review
93 | process (not necessarily the same person for each package). This coordination
94 | committee member (hereafter the *coordinator*) will find someone to carry out
95 | the main review of the package (hereafter the *reviewer*). In this sense the
96 | role of the coordinator is similar to that of a journal editor. However
97 | this is only partially true, because the coordinator (speaking for the
98 | coordination committee as a whole) may add their views to the review, unlike a
99 | journal editor who is often strictly impartial on the review itself.
100 | The pool of available reviewers will be anyone who has an official role on
101 | [the Astropy team](http://www.astropy.org/team.html).
102 |
103 | The coordinator sends out an email to possible reviewers one at a time to find
104 | someone who is willing to review the package, using the template at the bottom
105 | of this document. Let them know at this point that the coordinator will also be
106 | doing a review and synthesizing the results into a single review. Once a
107 | reviewer accepts, the coordinator sends them an email with detailed review
108 | instructions, using the template at the bottom of this document. The coordinator
109 | then leaves a message on the pull request saying:
110 |
111 | *Thank you for proposing this package as an affiliated package! I'm happy to
112 | confirm that your package is now under review and we'll post the results of
113 | the review here and on the mailing list.*
114 |
115 | Once the reviewer has finished, they send their review back by email to the
116 | coordinator, who then does their own review and then writes up a review using
117 | the review template which includes a table with badges (template at the bottom
118 | of this document). They then share it with the other coordination committee
119 | members to check for consensus. The review including the final decision (accept
120 | or reject) is then posted on the original pull request. If the package is
121 | rejected, the original author has a month to respond to any of the points in the
122 | review, and the coordinator may decide to change the decision.
123 |
124 | If the package is accepted, the coordinator sends a reply to the astropy-dev
125 | email to publish the decision, then also edits the pull request to add the
126 | results to the review to the JSON file. Once the continuous integration passes,
127 | the pull request is merged.
128 |
129 | If the package is rejected, the decision is posted to the pull request, and the
130 | pull request is closed.
131 |
132 | ## Templates
133 |
134 | ### Template email with review request
135 |
136 | Subject: Reviewing an Astropy-affiliated package submission
137 |
138 | Dear/Hi {potential reviewer},
139 |
140 | The Astropy project has received a request to consider the following as an
141 | affiliated package:
142 |
143 | Name:
144 | Description:
145 | Repository:
146 | Documentation:
147 |
148 | I am reaching out to ask whether you would be willing to review this package.
149 |
150 | To provide some background, an affiliated package is an astronomy-related Python
151 | package that is not part of the astropy core package, but is part of the Astropy
152 | Project community. Such as package demonstrate a commitment to Astropy’s goals
153 | of improving reuse, interoperability, and interface standards for Python
154 | astronomy and astrophysics packages.
155 |
156 | Reviewing a package involves reading over the documentation, having a look at
157 | the code to assess for example readability (but not read it line by line),
158 | checking how well the package is tested, as well as assessing its integration
159 | with the Astropy and wider astronomy ecosystem. Most packages can be reviewed
160 | within an hour and reviews are anonymous by default. If you accept this review,
161 | we will send you a detailed list of criteria to check for the package as well as
162 | how to submit your review.
163 |
164 | Could you reply to this email to let us know if you would be willing to review
165 | this package?
166 |
167 | Thanks!
168 | {coordinator_name}
169 |
170 | ### Template email with review instructions
171 |
172 | Dear/Hi {reviewer name},
173 |
174 | Thank you for accepting to review the following package for the Astropy project:
175 |
176 | Name:
177 |
178 | Description:
179 |
180 | Repository:
181 |
182 | You can find our review guidelines and instructions at the following address:
183 |
184 | https://github.com/astropy/astropy-procedures/blob/master/documents/affiliated_package_review_guidelines.md
185 |
186 | Thanks!
187 | {coordinator name}
188 |
189 | ### Template review markdown
190 |
191 | ```
192 | This package has been reviewed for inclusion in the Astropy affiliated package
193 | ecosystem by a member of the Astropy community as well as myself, and I have
194 | synthesized the results of the review here.
195 |
196 | You can find out more about our review criteria in
197 | [Reviewing affiliated packages](https://github.com/astropy/astropy-procedures/blob/master/documents/affiliated_package_review_guidelines.md).
198 | For each of the review categories below we have listed the score and have
199 | included some comments when the score is not green.
200 |
201 | *Remove the badges that aren't needed:*
202 |
203 |
261 |
262 | *Include any other comments here*
263 |
264 | *If accepted with all green:*
265 |
266 | **Summary/Decision**: Everything looks great, and we're happy to confirm that
267 | this package is accepted as an affiliated package! :trophy:
268 |
269 | *If accepted with some orange:*
270 |
271 | **Summary/Decision**: This package meets the review criteria for affiliated
272 | packages, so we are happy to confirm that we'll be listing your package as an
273 | affiliated package! Keep up the good work, and we encourage you to improve on
274 | the areas above that weren't “green” yet.
275 |
276 | *If there is any red:*
277 |
278 | **Summary/Decision**: Thanks for your work on this package! At the moment, we
279 | found some issues in some of the review areas. As per the review guidelines, we
280 | therefore won't be able to accept this package as an affiliated package yet.
281 | We will leave this pull request open for a month in case you would like to
282 | respond to the comments and/or address any of them.
283 |
284 | *In all cases:*
285 |
286 |
287 | If you have any follow-up questions or disagree with any of the comments above,
288 | leave a comment and we can discuss it here. Or if this has been merged but you
289 | want to suggest a change to the review status, you can open an PR in this repo
290 | updating your package's review status, along with an explanation of why you
291 | think it should be changed.
292 |
293 | At any point in future you or someone else can
294 | request a full re-review of the package if you believe substantial changes have been
295 | made and many of the scores should be updated - contact the coordination committee,
296 | and we’ll do a new review.
297 |
298 |
299 |
300 | ```
301 |
--------------------------------------------------------------------------------
/pr_consistency/4.check_consistency.py:
--------------------------------------------------------------------------------
1 | # This script takes the JSON files from the three previous scripts and runs
2 | # a whole bunch of consistency checks described in the comments below.
3 |
4 | import os
5 | import sys
6 | import json
7 | from datetime import datetime
8 | from collections import defaultdict
9 |
10 | from astropy.utils.console import color_print
11 |
12 | from common import get_branches
13 |
14 |
15 | def parse_isoformat(string):
16 | return datetime.strptime(string, "%Y-%m-%dT%H:%M:%S")
17 |
18 |
19 | # Only consider PRs merged after this date/time. At the moment, this is the
20 | # date and time at which the v1.0.x branch was created.
21 | START = parse_isoformat('2015-01-27T16:22:59')
22 |
23 | # The following option can be toggled to show only pull requests with issues or
24 | # show all pull requests.
25 | SHOW_VALID = False
26 |
27 | # If set to true, the output is suitable for viewing as a web page instead of
28 | # in the console
29 | HTML_OUTPUT = True
30 |
31 | # The repository to show the URL for easy access to PR changelogs. Can be None
32 | # To not show the url
33 |
34 | if sys.argv[1:]:
35 | REPOSITORY = sys.argv[1]
36 | else:
37 | REPOSITORY = 'astropy/astropy'
38 |
39 | print("The repository this script currently works with is '{}'.\n"
40 | .format(REPOSITORY))
41 |
42 | SHOW_URL_REPO = REPOSITORY
43 |
44 | NAME = os.path.basename(REPOSITORY)
45 |
46 | # The following colors are used to make the output more readabale. CANTFIX is
47 | # used for things that are issues that can never be resolved.
48 | VALID = 'green'
49 | CANTFIX = 'yellow'
50 | INVALID = 'red'
51 | BRANCHES = get_branches(REPOSITORY)
52 |
53 | # The following gives the dates when branches were closed. This helps us
54 | # understand later whether a pull request could have been backported to a given
55 | # branch.
56 |
57 | BRANCH_CLOSED_DICT = {'astropy/astropy': {
58 | 'v0.1.x': parse_isoformat('2012-06-19T02:09:53'),
59 | 'v0.2.x': parse_isoformat('2013-10-25T12:29:58'),
60 | 'v0.3.x': parse_isoformat('2014-05-13T12:06:04'),
61 | 'v0.4.x': parse_isoformat('2015-05-29T15:44:38'),
62 | 'v1.0.x': parse_isoformat('2017-05-29T23:44:38'),
63 | 'v1.1.x': parse_isoformat('2016-03-10T01:09:50'),
64 | 'v1.2.x': parse_isoformat('2016-12-23T05:32:04'),
65 | 'v1.3.x': parse_isoformat('2017-05-29T23:44:38'),
66 | 'v2.0.x': parse_isoformat('2019-11-10T16:00:00'),
67 | 'v3.0.x': parse_isoformat('2018-10-18T16:00:00'),
68 | 'v3.1.x': parse_isoformat('2019-04-15T16:00:00'),
69 | 'v3.2.x': parse_isoformat('2019-11-10T16:00:00'),
70 | 'v4.0.x': None,
71 | 'v4.1.x': parse_isoformat('2020-11-25T06:46:46'),
72 | 'v4.2.x': None,
73 | 'v4.3.x': None},
74 | }
75 |
76 | BRANCH_CLOSED_DICT['astropy/astropy-helpers'] = BRANCH_CLOSED_DICT['astropy/astropy']
77 |
78 | # We now list some exceptions, starting with manual merges/backports. This
79 | # gives for the specified pull requests the list of branches in which the
80 | # pull request was merged or was backported manually (but which won't show
81 | # up in the JSON file giving branches for each pull request). These pull
82 | # requests were merged manually without preserving a merge commit that
83 | # includes the original pull request number.
84 |
85 | # TODO: find a more future-proof way of including manual merges. At the moment,
86 | # when we add new branches, we'll need to add these branches to all
87 | # existing manual merges.
88 | MANUAL_MERGES_DICT = {
89 | 'astropy/astropy': {'8264': ('v2.0.x',),
90 | '7575': ('v2.0.x',),
91 | '7336': ('v2.0.x',),
92 | '7274': ('v2.0.x',),
93 | '6605': ('v2.0.x',),
94 | '6555': ('v2.0.x',),
95 | '6423': ('v2.0.x',),
96 | '4792': ('v1.2.x',),
97 | '4539': ('v1.0.x',),
98 | '4423': ('v1.2.x',),
99 | '4341': ('v1.1.x',),
100 | '4254': ('v1.0.x',),
101 | '4719': ('v1.2.x',),
102 | '4201': ('v1.0.x', 'v1.1.x', 'v1.2.x'),
103 | '9183': ('v3.2.x', 'v4.0.x', 'v4.1.x', 'v4.2.x'),
104 | '10437': ('v4.0.x',),
105 | '11108': ('v4.2.x'),
106 | '11128': ('v4.2.x'),
107 | '11145': ('v4.2.x'),
108 | '11389': ('v4.2.x'),
109 | '11391': ('v4.2.x'),
110 | '11401': ('v4.2.x'),
111 | '11250': ('v4.2.x'),
112 | '9183': ('v4.3.x'),
113 | },
114 | 'astropy/astropy-helpers': {'205': ('v1.1.x', 'v1.2.x', 'v1.3.x', 'v2.0.x',
115 | 'v3.0.x', 'v3.1.x', 'v3.2.x', 'v4.0.x'),
116 | '172': ('v1.1.x', 'v1.2.x', 'v1.3.x', 'v2.0.x',
117 | 'v3.0.x', 'v3.1.x', 'v3.2.x', 'v4.0.x'),
118 | '206': ('v1.0.x', 'v1.1.x', 'v1.2.x', 'v1.3.x',
119 | 'v2.0.x',
120 | 'v3.0.x', 'v3.1.x', 'v3.2.x', 'v4.0.x'),
121 | '362': ('v2.0.x')}
122 | }
123 |
124 |
125 | BRANCH_CLOSED = {}
126 | MANUAL_MERGES = {}
127 |
128 | try:
129 | BRANCH_CLOSED = BRANCH_CLOSED_DICT[REPOSITORY]
130 | MANUAL_MERGES = MANUAL_MERGES_DICT[REPOSITORY]
131 | except KeyError:
132 | pass
133 |
134 | # The following gives pull requests we know are missing from certain branches
135 | # and which we will never be able to backport since those branches are closed.
136 |
137 | EXPECTED_MISSING = {
138 | '4266': ('v1.1.x',), # Forgot to backport to v1.1.x
139 | }
140 |
141 | REVERTED_FROM_BRANCH ={
142 | '6277': ('v2.0.x',), # PR has been reverted from this branch
143 | }
144 |
145 | # The following pull requests appear as merged on GitHub but were actually
146 | # marked as merged by another pull request getting merge and including a
147 | # superset of the original commits.
148 |
149 | CLOSED_BY_ANOTHER = {
150 | '3624': '3697',
151 | '2676': '2680'
152 | }
153 |
154 | with open(f'merged_pull_requests_{NAME}.json') as merged:
155 | merged_prs = json.load(merged)
156 |
157 | with open(f'pull_requests_changelog_sections_{NAME}.json') as merged:
158 | changelog_prs = json.load(merged)
159 |
160 | with open(f'pull_requests_branches_{NAME}.json') as merged:
161 | pr_branches = json.load(merged)
162 |
163 |
164 | if HTML_OUTPUT:
165 | print('\nAstropy Consistency Check Report'
166 | '\n\nMain report for repository {}
'.format(REPOSITORY))
167 | else:
168 | color_print('Main report:', 'blue')
169 |
170 | backports = defaultdict(list)
171 |
172 | for pr in sorted(merged_prs, key=lambda pr: merged_prs[pr]['merged']):
173 |
174 | if 'unusual-merge-dealt-with' in merged_prs[pr]['labels']:
175 | # This label indicates problematic PRs that have been checked
176 | # manually and don't need to be considered here.
177 | continue
178 |
179 | merge_date = parse_isoformat(merged_prs[pr]['merged'])
180 |
181 | if merge_date < START:
182 | continue
183 |
184 | if pr in CLOSED_BY_ANOTHER:
185 | continue
186 |
187 | # Extract labels and milestones/versions
188 | labels = merged_prs[pr]['labels']
189 | milestone = merged_prs[pr]['milestone']
190 | if milestone is not None and not milestone.startswith('v') and milestone != 'Future':
191 | milestone = 'v' + milestone
192 |
193 | cl_version = changelog_prs.get(pr, None)
194 |
195 | if cl_version:
196 | # Ignore RC status in changelog, those are temporary measures
197 | cl_version = cl_version.split('rc')[0]
198 |
199 | branches = pr_branches.get(pr, [])
200 |
201 | status = []
202 | valid = True
203 |
204 | # Make sure that the milestone is consistent with the changelog section, and
205 | # that this is also consistent with the labels set on the pull request.
206 |
207 | affect_dev = {'Affects-dev', 'affects-dev', 'affect-dev', 'Affect-dev'}
208 | affect_dev_in_labels = len(affect_dev.intersection(set(labels))) > 0
209 |
210 | if pr in changelog_prs:
211 | if affect_dev_in_labels:
212 | pass # don't print for now since there are too many
213 | # status.append(('Labelled as affects-dev but in changelog ({0})'.format(cl_version), INVALID))
214 | elif 'no-changelog-entry-needed' in labels:
215 | status.append(('Labelled as no-changelog-entry-needed but in changelog', INVALID))
216 | else:
217 | if milestone is None:
218 | status.append(f'In changelog ({cl_version}) but not milestoned')
219 | elif milestone.startswith(cl_version):
220 | status.append((f'In correct section of changelog ({cl_version})', VALID))
221 | else:
222 | status.append((f'Milestone is {milestone} but change log section is {cl_version}', INVALID))
223 | else:
224 | if affect_dev_in_labels:
225 | status.append(('Labelled as affects-dev and not in changelog', VALID))
226 | elif 'no-changelog-entry-needed' in labels:
227 | status.append(('Labelled as no-changelog-entry-needed and not in changelog', VALID))
228 | else:
229 | if milestone is None:
230 | status.append(('Not in changelog (and no milestone) but not labelled affects-dev', INVALID))
231 | else:
232 | if milestone.startswith('v0.1'):
233 | status.append((f'Not in changelog (but ok since milestoned as {milestone})', VALID))
234 | else:
235 | status.append((f'Not in changelog (milestoned as {milestone}) but not labelled as affects-dev', INVALID))
236 |
237 | # Now check for consistency between PR milestone and branch in which the PR
238 | # appears - can only check this if the PR milestone is set. If it isn't
239 | # set, the above will emit a warning, and once a milestone is then set we
240 | # can rerun this and check for errors below.
241 |
242 | if milestone is not None:
243 |
244 | earliest_expected_branch = milestone[0:4] + '.x'
245 |
246 | if earliest_expected_branch in BRANCHES:
247 |
248 | index = BRANCHES.index(earliest_expected_branch)
249 |
250 | # We now make sure that the PR does NOT appear until ``index``,
251 | # then is there all the time.
252 |
253 | for i in range(index):
254 | if BRANCHES[i] in branches:
255 | if BRANCHES[i] in REVERTED_FROM_BRANCH.get(pr, []):
256 | status.append((f'Pull request was in branc {BRANCHES[i]} but has been reverted later.', VALID))
257 | else:
258 | status.append((f'Pull request was included in branch {BRANCHES[i]}', INVALID))
259 | else:
260 | pass # all good
261 |
262 | for i in range(index, len(BRANCHES)):
263 | if BRANCHES[i] in branches:
264 | status.append((f'Pull request was included in branch {BRANCHES[i]}', VALID))
265 | else:
266 | if BRANCHES[i] in MANUAL_MERGES.get(pr, []):
267 | status.append((f'Pull request was included in branch {BRANCHES[i]} (manually merged)', VALID))
268 | elif BRANCHES[i] in EXPECTED_MISSING.get(pr, []):
269 | status.append((f'Pull request was not included in branch {BRANCHES[i]} (but whitelisted as ok)', VALID))
270 | else:
271 | if BRANCH_CLOSED[BRANCHES[i]] is not None:
272 | if merge_date > BRANCH_CLOSED[BRANCHES[i]]:
273 | status.append((f'Pull request was not included in branch {BRANCHES[i]} (but was merged after branch closed)', VALID))
274 | else:
275 | status.append((f'Pull request was not included in branch {BRANCHES[i]} (but too late to fix)', CANTFIX))
276 | else:
277 | status.append((f'Pull request was not included in branch {BRANCHES[i]}. Backport command included below.', INVALID))
278 | backports[BRANCHES[i]].append(pr)
279 |
280 | else:
281 | pass # no branch for this milestone yet
282 |
283 | # If SHOW_VALID is False, we want to skip entries which are all valid.
284 | # Otherwise we want to show both valid and invalid entries.
285 |
286 | if not SHOW_VALID:
287 | for msg in status:
288 | if INVALID in msg:
289 | break
290 | else:
291 | continue
292 |
293 | url = ''
294 | if SHOW_URL_REPO:
295 | url = f'https://github.com/{SHOW_URL_REPO}/issues/{pr}'
296 | if HTML_OUTPUT:
297 | print('')
298 | print(f'#{pr} (Milestone: {milestone})')
299 | print('
')
300 | for msg, color in status:
301 | print(f'- {msg}
')
302 | print('
\n
')
303 | else:
304 | print("#{0}{2} (Milestone: {1})".format(pr, milestone, ' ('+url+')'))
305 | for msg in status:
306 | color_print(' - ', '', *msg)
307 |
308 | for version in sorted(backports.keys()):
309 | if HTML_OUTPUT:
310 | print(f'Backports to {version}
')
311 | print('{} merges in total. These are in merge order:'.format(
312 | len(backports[version])))
313 | print('')
314 | for pr in backports[version]:
315 | prorurl = '#{}'.format('pr')
316 | if SHOW_URL_REPO:
317 | url = f'https://github.com/{SHOW_URL_REPO}/issues/{pr}'
318 | prorurl = f'#{pr}'
319 |
320 | print('# Pull request {}: {}'.format(prorurl, merged_prs[pr]['title']))
321 | print('git cherry-pick -m 1 {}'.format(merged_prs[pr]['merge_commit']))
322 | print('')
323 | else:
324 | color_print(f'Backports to {version} (in merge order)', 'blue')
325 | for pr in backports[version]:
326 | print('# Pull request #{}: {}'.format(pr, merged_prs[pr]['title']))
327 | print('git cherry-pick -m 1 {}'.format(merged_prs[pr]['merge_commit']))
328 |
--------------------------------------------------------------------------------
/visualizations_demographics/Travis Build Info near Feature Freeze.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {
7 | "collapsed": true
8 | },
9 | "outputs": [],
10 | "source": [
11 | "import json\n",
12 | "import gzip\n",
13 | "\n",
14 | "import tqdm\n",
15 | "\n",
16 | "import numpy as np\n",
17 | "\n",
18 | "from astropy.time import Time\n",
19 | "from astropy import units as u\n",
20 | "\n",
21 | "%matplotlib inline\n",
22 | "from matplotlib import pyplot as plt"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 2,
28 | "metadata": {},
29 | "outputs": [],
30 | "source": [
31 | "# this file comes from running the get_travis_builds_info.py script\n",
32 | "with gzip.open('travis_build_info.json.gz', 'rt') as f:\n",
33 | " build_info = json.load(f)"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": 3,
39 | "metadata": {},
40 | "outputs": [
41 | {
42 | "data": {
43 | "text/plain": [
44 | "(878, 25, 17312)"
45 | ]
46 | },
47 | "execution_count": 3,
48 | "metadata": {},
49 | "output_type": "execute_result"
50 | }
51 | ],
52 | "source": [
53 | "all_commit_times = []\n",
54 | "commit_times = []\n",
55 | "start_times = []\n",
56 | "update_times = []\n",
57 | "\n",
58 | "all_skipped = []\n",
59 | "skipped = []\n",
60 | "for i, b in enumerate(build_info):\n",
61 | " all_commit_times.append(b['commit']['committed_at'])\n",
62 | " commit_times.append(all_commit_times[-1])\n",
63 | " start_times.append(b['started_at'])\n",
64 | " update_times.append(b['updated_at'])\n",
65 | " \n",
66 | " if all_commit_times[-1] is None:\n",
67 | " del all_commit_times[-1]\n",
68 | " all_skipped.append(i)\n",
69 | " \n",
70 | " if commit_times[-1] is None or start_times[-1] is None or update_times[-1] is None:\n",
71 | " del commit_times[-1]\n",
72 | " del start_times[-1]\n",
73 | " del update_times[-1]\n",
74 | " skipped.append(i)\n",
75 | " \n",
76 | "all_commit_times = Time(all_commit_times)\n",
77 | "commit_times = Time(commit_times)\n",
78 | "start_times = Time(start_times)\n",
79 | "update_times = Time(update_times)\n",
80 | " \n",
81 | "len(skipped), len(all_skipped), len(commit_times)"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": 4,
87 | "metadata": {
88 | "collapsed": true
89 | },
90 | "outputs": [],
91 | "source": [
92 | "freeze_times = {}\n",
93 | "freeze_times['2.0'] = Time('2017-6-18')\n",
94 | "freeze_times['1.3'] = Time('2016-12-7')\n",
95 | "freeze_times['1.2'] = Time('2016-5-13')\n",
96 | "freeze_times['1.1'] = Time('2015-10-15')\n",
97 | "freeze_times['1.0'] = Time('2014-12-19')"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": 5,
103 | "metadata": {},
104 | "outputs": [
105 | {
106 | "data": {
107 | "image/png": "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\n",
108 | "text/plain": [
109 | ""
110 | ]
111 | },
112 | "metadata": {},
113 | "output_type": "display_data"
114 | }
115 | ],
116 | "source": [
117 | "plt.plot_date(np.sort(all_commit_times.plot_date), np.arange(len(all_commit_times)), '-')\n",
118 | "\n",
119 | "for t in freeze_times.values():\n",
120 | " plt.axvline(t.plot_date, c='k', ls=':')"
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": 6,
126 | "metadata": {},
127 | "outputs": [
128 | {
129 | "data": {
130 | "image/png": "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\nP32ZVz8vsgAAAABJRU5ErkJggg==\n",
131 | "text/plain": [
132 | ""
133 | ]
134 | },
135 | "metadata": {},
136 | "output_type": "display_data"
137 | }
138 | ],
139 | "source": [
140 | "fig, axs = plt.subplots(len(freeze_times), 1, figsize=(8, 12))\n",
141 | "\n",
142 | "for vers, ax in zip(sorted(freeze_times), axs):\n",
143 | " t = freeze_times[vers]\n",
144 | " lower = t - 10*u.day\n",
145 | " upper = t + 4*u.day\n",
146 | " inwindow = all_commit_times[(lower