1# Copyright 2021-2022 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Invoke tasks 17""" 18 19# ----------------------------------------------------------------------------- 20# Imports 21# ----------------------------------------------------------------------------- 22import os 23import glob 24import shutil 25import urllib 26from pathlib import Path 27from invoke import task, call, Collection 28from invoke.exceptions import Exit, UnexpectedExit 29 30 31# ----------------------------------------------------------------------------- 32ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) 33 34ns = Collection() 35 36 37# ----------------------------------------------------------------------------- 38# Build 39# ----------------------------------------------------------------------------- 40build_tasks = Collection() 41ns.add_collection(build_tasks, name="build") 42 43 44# ----------------------------------------------------------------------------- 45@task 46def build(ctx, install=False): 47 if install: 48 ctx.run('python -m pip install .[build]') 49 50 ctx.run("python -m build") 51 52 53# ----------------------------------------------------------------------------- 54@task 55def release_build(ctx): 56 build(ctx, install=True) 57 58 59# ----------------------------------------------------------------------------- 60@task 61def mkdocs(ctx): 62 ctx.run("mkdocs build -f docs/mkdocs/mkdocs.yml") 63 64 65# ----------------------------------------------------------------------------- 66build_tasks.add_task(build, default=True) 67build_tasks.add_task(release_build, name="release") 68build_tasks.add_task(mkdocs, name="mkdocs") 69 70 71# ----------------------------------------------------------------------------- 72# Test 73# ----------------------------------------------------------------------------- 74test_tasks = Collection() 75ns.add_collection(test_tasks, name="test") 76 77 78# ----------------------------------------------------------------------------- 79@task(incrementable=["verbose"]) 80def test(ctx, match=None, junit=False, install=False, html=False, verbose=0): 81 # Install the package before running the tests 82 if install: 83 ctx.run("python -m pip install .[test]") 84 85 args = "" 86 if junit: 87 args += "--junit-xml test-results.xml" 88 if match is not None: 89 args += f" -k '{match}'" 90 if html: 91 args += " --html results.html" 92 if verbose > 0: 93 args += f" -{'v' * verbose}" 94 ctx.run(f"python -m pytest {os.path.join(ROOT_DIR, 'tests')} {args}") 95 96 97# ----------------------------------------------------------------------------- 98@task 99def release_test(ctx): 100 test(ctx, install=True) 101 102 103# ----------------------------------------------------------------------------- 104test_tasks.add_task(test, default=True) 105test_tasks.add_task(release_test, name="release") 106 107# ----------------------------------------------------------------------------- 108# Project 109# ----------------------------------------------------------------------------- 110project_tasks = Collection() 111ns.add_collection(project_tasks, name="project") 112 113 114# ----------------------------------------------------------------------------- 115@task 116def lint(ctx, disable='C,R', errors_only=False): 117 options = [] 118 if disable: 119 options.append(f"--disable={disable}") 120 if errors_only: 121 options.append("-E") 122 123 if errors_only: 124 qualifier = ' (errors only)' 125 else: 126 qualifier = f' (disabled: {disable})' if disable else '' 127 128 print(f">>> Running the linter{qualifier}...") 129 try: 130 ctx.run(f"pylint {' '.join(options)} bumble apps examples tasks.py") 131 print("The linter is happy. ✅ ") 132 except UnexpectedExit as exc: 133 print("Please check your code against the linter messages. ❌") 134 raise Exit(code=1) from exc 135 136 137# ----------------------------------------------------------------------------- 138@task 139def format_code(ctx, check=False, diff=False): 140 options = [] 141 if check: 142 options.append("--check") 143 if diff: 144 options.append("--diff") 145 146 print(">>> Running the formatter...") 147 try: 148 ctx.run(f"black -S {' '.join(options)} .") 149 except UnexpectedExit as exc: 150 print("Please run 'invoke project.format' or 'black .' to format the code. ❌") 151 raise Exit(code=1) from exc 152 153 154# ----------------------------------------------------------------------------- 155@task 156def check_types(ctx): 157 checklist = ["apps", "bumble", "examples", "tests", "tasks.py"] 158 try: 159 ctx.run(f"mypy {' '.join(checklist)}") 160 except UnexpectedExit as exc: 161 print("Please check your code against the mypy messages.") 162 raise Exit(code=1) from exc 163 164 165# ----------------------------------------------------------------------------- 166@task( 167 pre=[ 168 call(format_code, check=True), 169 call(lint, errors_only=True), 170 call(check_types), 171 test, 172 ] 173) 174def pre_commit(_ctx): 175 print("All good!") 176 177 178# ----------------------------------------------------------------------------- 179project_tasks.add_task(lint) 180project_tasks.add_task(format_code, name="format") 181project_tasks.add_task(check_types, name="check-types") 182project_tasks.add_task(pre_commit) 183 184 185# ----------------------------------------------------------------------------- 186# Web 187# ----------------------------------------------------------------------------- 188web_tasks = Collection() 189ns.add_collection(web_tasks, name="web") 190 191 192# ----------------------------------------------------------------------------- 193@task 194def serve(ctx, port=8000): 195 """ 196 Run a simple HTTP server for the examples under the `web` directory. 197 """ 198 import http.server 199 200 address = ("", port) 201 202 class Handler(http.server.SimpleHTTPRequestHandler): 203 def __init__(self, *args, **kwargs): 204 super().__init__(*args, directory="web", **kwargs) 205 206 server = http.server.HTTPServer(address, Handler) 207 print(f"Now serving on port {port} ️") 208 server.serve_forever() 209 210 211# ----------------------------------------------------------------------------- 212@task 213def web_build(ctx): 214 # Step 1: build the wheel 215 build(ctx) 216 # Step 2: Copy the wheel to the web folder, so the http server can access it 217 newest_wheel = Path(max(glob.glob('dist/*.whl'), key=lambda f: os.path.getmtime(f))) 218 shutil.copy(newest_wheel, Path('web/')) 219 # Step 3: Write wheel's name to web/packageFile 220 with open(Path('web', 'packageFile'), mode='w') as package_file: 221 package_file.write(str(Path('/') / newest_wheel.name)) 222 # Step 4: Success! 223 print('Include ?packageFile=true in your URL!') 224 225 226# ----------------------------------------------------------------------------- 227web_tasks.add_task(serve) 228web_tasks.add_task(web_build, name="build") 229