├── 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    RedAffiliated 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.
    OrangeHaving 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.
    GreenThis 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.
    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 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
    Out of scopeNot useful for astronomers, or specific to one project/collaboration.
    Specialized packageUseful 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.
    GreenPackage that is useful for astronomers across more than a single field/instrument/telescope. Packages such as astroquery or astroplan fall into this category.
    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 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
    RedDoes not use Astropy or other affiliated packages anywhere where it should be possible, and/or uses other libraries instead.
    OrangeMakes an effort to use Astropy or other affiliated packages in places, but still has other places where this could be done but isn’t
    GreenUses Astropy or other affiliated packages wherever possible.
    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 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
    RedNo documentation or some documentation, but very bare bones/minimal and incomplete or incorrect in a number of places.
    OrangeReasonable documentation (which could be a very well written README), installation instructions and at least one usage example, but some parts missing.
    GreenExtensive 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.
    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 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
    RedNo 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).
    OrangeA 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.
    GreenTest 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.
    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 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |
    RedNo active development/maintainers, even if stable releases exist, if package is not or no longer fully functional.
    Heavy DevelopmentStable releases exist, but still under heavy development (so API changes can be frequent).
    Functional but unmaintainedStable releases exist and there are no active developers/maintainers but package remains mostly functional.
    Functional but low activityStable 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')
    GreenPackage 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.
    165 | 166 | ### Python 3 compatibility ('python3') 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
    RedNothing is currently 'unacceptable'. Starting 1 January 2020, 'Not compatible with Python 3' will be red.
    OrangeNot compatible with Python 3
    GreenCompatible with Python 3
    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 | fork 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 | commit 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 | create_1 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 | create_2 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 | 204 | 210 | 211 | 212 | 213 | 219 | 220 | 221 | 222 | 228 | 229 | 230 | 231 | 237 | 238 | 239 | 240 | 248 | 249 | 250 | 251 | 257 | 258 | 259 | 260 |
    Functionality/Scope 205 | 206 | Out of scope 207 | Specialized package 208 | General package 209 |
    No further comments
    Integration with Astropy ecosystem 214 | 215 | Red 216 | Orange 217 | Green 218 |
    No further comments
    Documentation 223 | 224 | Red 225 | Orange 226 | Green 227 |
    No further comments
    Testing 232 | 233 | Red 234 | Orange 235 | Green 236 |
    No further comments
    Development status 241 | 242 | Red 243 | Heavy Development 244 | Functional but unmaintained 245 | Functional but low activity 246 | Green 247 |
    No further comments
    Python 3 compatibility 252 | 253 | Red 254 | Orange 255 | Green 256 |
    No further comments
    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\n

    Main 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