All defs are now typed

This commit is contained in:
Kovid Goyal 2020-03-14 14:37:11 +05:30
parent d3f37eeba4
commit 5956277863
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 272 additions and 191 deletions

View File

@ -4,12 +4,12 @@
import subprocess
from collections import defaultdict
from typing import DefaultDict, Dict, FrozenSet, List, Tuple, Union
from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union
KeymapType = Dict[str, Tuple[str, Union[FrozenSet[str], str]]]
def resolve_keys(keymap: KeymapType):
def resolve_keys(keymap: KeymapType) -> DefaultDict[str, List[str]]:
ans: DefaultDict[str, List[str]] = defaultdict(list)
for ch, (attr, atype) in keymap.items():
if isinstance(atype, str) and atype in ('int', 'uint'):
@ -20,7 +20,7 @@ def resolve_keys(keymap: KeymapType):
return ans
def enum(keymap: KeymapType):
def enum(keymap: KeymapType) -> str:
lines = []
for ch, (attr, atype) in keymap.items():
lines.append(f"{attr}='{ch}'")
@ -31,7 +31,7 @@ def enum(keymap: KeymapType):
'''.format(',\n'.join(lines))
def parse_key(keymap: KeymapType):
def parse_key(keymap: KeymapType) -> str:
lines = []
for attr, atype in keymap.values():
vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG'
@ -39,7 +39,7 @@ def parse_key(keymap: KeymapType):
return ' \n'.join(lines)
def parse_flag(keymap: KeymapType, type_map, command_class):
def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str) -> str:
lines = []
for ch in type_map['flag']:
attr, allowed_values = keymap[ch]
@ -57,14 +57,14 @@ def parse_flag(keymap: KeymapType, type_map, command_class):
return ' \n'.join(lines)
def parse_number(keymap: KeymapType):
def parse_number(keymap: KeymapType) -> Tuple[str, str]:
int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
return '; '.join(int_keys), '; '.join(uint_keys)
def cmd_for_report(report_name, keymap: KeymapType, type_map, payload_allowed):
def group(atype, conv):
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any], payload_allowed: bool) -> str:
def group(atype: str, conv: str) -> Tuple[str, str]:
flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
for ch in type_map[atype]:
@ -89,7 +89,15 @@ def cmd_for_report(report_name, keymap: KeymapType, type_map, payload_allowed):
return '\n'.join(ans)
def generate(function_name, callback_name, report_name, keymap: KeymapType, command_class, initial_key='a', payload_allowed=True):
def generate(
function_name: str,
callback_name: str,
report_name: str,
keymap: KeymapType,
command_class: str,
initial_key: str = 'a',
payload_allowed: bool = True
) -> str:
type_map = resolve_keys(keymap)
keys_enum = enum(keymap)
handle_key = parse_key(keymap)
@ -230,7 +238,7 @@ static inline void
'''
def write_header(text, path):
def write_header(text: str, path: str) -> None:
with open(path, 'w') as f:
print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n')
print('#pragma once', file=f)
@ -238,7 +246,7 @@ def write_header(text, path):
subprocess.check_call(['clang-format', '-i', path])
def graphics_parser():
def graphics_parser() -> None:
flag = frozenset
keymap: KeymapType = {
'a': ('action', flag('tTqpd')),

View File

@ -13,7 +13,8 @@ from html.entities import html5
from itertools import groupby
from operator import itemgetter
from typing import (
DefaultDict, Dict, Generator, Iterable, List, Optional, Set, Tuple, Union
Callable, DefaultDict, Dict, FrozenSet, Generator, Iterable, List,
Optional, Set, Tuple, Union
)
from urllib.request import urlopen
@ -27,7 +28,7 @@ if len(non_characters) != 66:
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
def get_data(fname, folder='UCD'):
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
bn = os.path.basename(url)
local = os.path.join('/tmp', bn)
@ -54,9 +55,9 @@ marks = set(emoji_skin_tone_modifiers) | {zwj}
not_assigned = set(range(0, sys.maxunicode))
def parse_ucd():
def parse_ucd() -> None:
def add_word(w, c):
def add_word(w: str, c: int) -> None:
if c <= 32 or c == 127 or 128 <= c <= 159:
return
if len(w) > 1:
@ -102,7 +103,7 @@ def parse_ucd():
word_search_map['lamda'] |= word_search_map['lambda']
def split_two(line):
def split_two(line: str) -> Tuple[Set[int], str]:
spec, rest = line.split(';', 1)
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].strip()
if '..' in spec:
@ -118,26 +119,27 @@ emoji_categories: Dict[str, Set[int]] = {}
emoji_presentation_bases: Set[int] = set()
def parse_emoji():
def parse_emoji() -> None:
for line in get_data('emoji-data.txt', 'emoji'):
chars, rest = split_two(line)
s = emoji_categories.setdefault(rest, set())
s.update(chars)
all_emoji.update(chars)
for line in get_data('emoji-variation-sequences.txt', 'emoji'):
base, var, *rest = line.split()
parts = line.split()
base, var = parts[0], parts[1]
if base.startswith('#'):
continue
base = int(base, 16)
if var.upper() == 'FE0F':
emoji_presentation_bases.add(base)
ibase = int(base, 16)
emoji_presentation_bases.add(ibase)
doublewidth: Set[int] = set()
ambiguous: Set[int] = set()
def parse_eaw():
def parse_eaw() -> None:
global doublewidth, ambiguous
seen: Set[int] = set()
for line in get_data('ucd/EastAsianWidth.txt'):
@ -166,7 +168,7 @@ def get_ranges(items: List[int]) -> Generator[Union[int, Tuple[int, int]], None,
yield a, b
def write_case(spec, p):
def write_case(spec: Union[Tuple, int], p: Callable) -> None:
if isinstance(spec, tuple):
p('\t\tcase 0x{:x} ... 0x{:x}:'.format(*spec))
else:
@ -174,7 +176,7 @@ def write_case(spec, p):
@contextmanager
def create_header(path, include_data_types=True):
def create_header(path: str, include_data_types: bool = True) -> Generator[Callable, None, None]:
with open(path, 'w') as f:
p = partial(print, file=f)
p('// unicode data, built from the unicode standard on:', date.today())
@ -191,7 +193,7 @@ def create_header(path, include_data_types=True):
p('END_ALLOW_CASE_RANGE')
def gen_emoji():
def gen_emoji() -> None:
with create_header('kitty/emoji.h') as p:
p('static inline bool\nis_emoji(char_type code) {')
p('\tswitch(code) {')
@ -221,8 +223,16 @@ def gen_emoji():
p('\treturn false;\n}')
def category_test(name, p, classes, comment, static=False, extra_chars=frozenset(), exclude=frozenset()):
static = 'static inline ' if static else ''
def category_test(
name: str,
p: Callable,
classes: Iterable[str],
comment: str,
use_static: bool = False,
extra_chars: Union[FrozenSet[int], Set[int]] = frozenset(),
exclude: Union[Set[int], FrozenSet[int]] = frozenset()
) -> None:
static = 'static inline ' if use_static else ''
chars: Set[int] = set()
for c in classes:
chars |= class_maps[c]
@ -238,7 +248,7 @@ def category_test(name, p, classes, comment, static=False, extra_chars=frozenset
p('\treturn false;\n}\n')
def codepoint_to_mark_map(p, mark_map):
def codepoint_to_mark_map(p: Callable, mark_map: List[int]) -> Dict[int, int]:
p('\tswitch(c) { // {{{')
rmap = {c: m for m, c in enumerate(mark_map)}
for spec in get_ranges(mark_map):
@ -253,14 +263,14 @@ def codepoint_to_mark_map(p, mark_map):
return rmap
def classes_to_regex(classes, exclude=''):
def classes_to_regex(classes: Iterable[str], exclude: str = '') -> Iterable[str]:
chars: Set[int] = set()
for c in classes:
chars |= class_maps[c]
for c in map(ord, exclude):
chars.discard(c)
for x in map(ord, exclude):
chars.discard(x)
def as_string(codepoint):
def as_string(codepoint: int) -> str:
if codepoint < 256:
return r'\x{:02x}'.format(codepoint)
if codepoint <= 0xffff:
@ -274,7 +284,7 @@ def classes_to_regex(classes, exclude=''):
yield as_string(spec)
def gen_ucd():
def gen_ucd() -> None:
cz = {c for c in class_maps if c[0] in 'CZ'}
with create_header('kitty/unicode-data.c') as p:
p('#include "unicode-data.h"')
@ -313,7 +323,7 @@ def gen_ucd():
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n'))))
def gen_names():
def gen_names() -> None:
with create_header('kittens/unicode_input/names.h') as p:
mark_to_cp = list(sorted(name_map))
cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
@ -372,24 +382,24 @@ def gen_names():
class TrieNode:
def __init__(self):
def __init__(self) -> None:
self.match_offset = 0
self.children_offset = 0
self.children = {}
self.children: Dict[int, int] = {}
def add_letter(self, letter):
def add_letter(self, letter: int) -> int:
if letter not in self.children:
self.children[letter] = len(all_trie_nodes)
all_trie_nodes.append(TrieNode())
return self.children[letter]
def __str__(self):
def __str__(self) -> str:
return f'{{ .children_offset={self.children_offset}, .match_offset={self.match_offset} }}'
root = TrieNode()
all_trie_nodes.append(root)
def add_word(word_idx, word):
def add_word(word_idx: int, word: str) -> None:
parent = root
for letter in map(ord, word):
idx = parent.add_letter(letter)
@ -414,10 +424,10 @@ def gen_names():
p('}; // }}}\n')
def gen_wcwidth():
def gen_wcwidth() -> None:
seen: Set[int] = set()
def add(p, comment, chars_, ret):
def add(p: Callable, comment: str, chars_: Set[int], ret: int) -> None:
chars = chars_ - seen
seen.update(chars)
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')

View File

@ -6,18 +6,54 @@ import json
import os
import re
import sys
from typing import Callable, List, Optional, Tuple
_plat = sys.platform.lower()
is_linux = 'linux' in _plat
base = os.path.dirname(os.path.abspath(__file__))
def wayland_protocol_file_name(base, ext='c'):
class Env:
cc: str = ''
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
ldpaths: List[str] = []
ccver: Tuple[int, int]
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: str = '', cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0)
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.ldpaths = cc, cppflags, cflags, ldflags, [] if ldpaths is None else ldpaths
self.ccver = ccver
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
return ans
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return 'wayland-{}-client-protocol.{}'.format(base, ext)
def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
def init_env(env: Env, pkg_config: Callable, at_least_version: Callable, test_compile: Callable, module: str = 'x11') -> Env:
ans = env.copy()
ans.cflags.append('-fpic')
ans.cppflags.append('-D_GLFW_' + module.upper())
@ -74,7 +110,7 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
return ans
def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_dir):
def build_wayland_protocols(env: Env, Command: Callable, parallel_run: Callable, emphasis: Callable, newer: Callable, dest_dir: str) -> None:
items = []
for protocol in env.wayland_protocols:
src = os.path.join(env.wayland_packagedir, protocol)
@ -95,7 +131,7 @@ def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_di
class Arg:
def __init__(self, decl):
def __init__(self, decl: str):
self.type, self.name = decl.rsplit(' ', 1)
self.type = self.type.strip()
self.name = self.name.strip()
@ -103,13 +139,13 @@ class Arg:
self.name = self.name[1:]
self.type = self.type + '*'
def __repr__(self):
def __repr__(self) -> str:
return 'Arg({}, {})'.format(self.type, self.name)
class Function:
def __init__(self, declaration, check_fail=True):
def __init__(self, declaration: str, check_fail: bool = True):
self.check_fail = check_fail
m = re.match(
r'(.+?)\s+(glfw[A-Z][a-zA-Z0-9]+)[(](.+)[)]$', declaration
@ -128,14 +164,14 @@ class Function:
if not self.args:
self.args = [Arg('void v')]
def declaration(self):
def declaration(self) -> str:
return 'typedef {restype} (*{name}_func)({args});\n{name}_func {name}_impl;\n#define {name} {name}_impl'.format(
restype=self.restype,
name=self.name,
args=', '.join(a.type for a in self.args)
)
def load(self):
def load(self) -> str:
ans = '*(void **) (&{name}_impl) = dlsym(handle, "{name}");'.format(
name=self.name
)
@ -146,7 +182,7 @@ class Function:
return ans
def generate_wrappers(glfw_header):
def generate_wrappers(glfw_header: str) -> None:
with open(glfw_header) as f:
src = f.read()
functions = []
@ -244,7 +280,7 @@ unload_glfw(void) {
f.write(code)
def main():
def main() -> None:
os.chdir(os.path.dirname(os.path.abspath(__file__)))
generate_wrappers('glfw3.h')

View File

@ -16,7 +16,7 @@ import sys
import tempfile
import time
from contextlib import suppress
from typing import IO, Optional, cast
from typing import IO, Any, Dict, Iterable, List, Optional, cast
import requests
@ -44,7 +44,7 @@ def call(*cmd: str, cwd: Optional[str] = None) -> None:
raise SystemExit(ret)
def run_build(args):
def run_build(args: Any) -> None:
os.chdir(build_path)
call('./linux 64 kitty')
call('./osx kitty --sign-installers')
@ -52,21 +52,21 @@ def run_build(args):
call('./linux 32 kitty')
def run_tag(args):
def run_tag(args: Any) -> None:
call('git push')
call('git tag -s v{0} -m version-{0}'.format(version))
call('git push origin v{0}'.format(version))
def run_man(args):
def run_man(args: Any) -> None:
call('make FAIL_WARN=-W man', cwd=docs_dir)
def run_html(args):
def run_html(args: Any) -> None:
call('make FAIL_WARN=-W html', cwd=docs_dir)
def add_analytics():
def add_analytics() -> None:
analytics = '''
<!-- Google Analytics -->
<script>
@ -87,7 +87,7 @@ ga('send', 'pageview');
f.write(html.encode('utf-8'))
def run_website(args):
def run_website(args: Any) -> None:
if os.path.exists(publish_dir):
shutil.rmtree(publish_dir)
shutil.copytree(os.path.join(docs_dir, '_build', 'html'), publish_dir)
@ -105,7 +105,7 @@ def run_website(args):
subprocess.check_call(['git', 'push'])
def run_sdist(args):
def run_sdist(args: Any) -> None:
with tempfile.TemporaryDirectory() as tdir:
base = os.path.join(tdir, f'kitty-{version}')
os.mkdir(base)
@ -122,7 +122,7 @@ def run_sdist(args):
class ReadFileWithProgressReporting(io.FileIO): # {{{
def __init__(self, path):
def __init__(self, path: str):
io.FileIO.__init__(self, path, 'rb')
self.seek(0, os.SEEK_END)
self._total = self.tell()
@ -166,14 +166,12 @@ class ReadFileWithProgressReporting(io.FileIO): # {{{
class Base(object): # {{{
def __init__(self):
pass
def info(self, *args, **kwargs):
def info(self, *args: Any, **kwargs: Any) -> None:
print(*args, **kwargs)
sys.stdout.flush()
def warn(self, *args, **kwargs):
def warn(self, *args: Any, **kwargs: Any) -> None:
print('\n' + '_' * 20, 'WARNING', '_' * 20)
print(*args, **kwargs)
print('_' * 50)
@ -187,13 +185,15 @@ class GitHub(Base): # {{{
API = 'https://api.github.com/'
def __init__(self,
files,
reponame,
version,
username,
password,
replace=False):
def __init__(
self,
files: Dict[str, str],
reponame: str,
version: str,
username: str,
password: str,
replace: bool = False
):
self.files, self.reponame, self.version, self.username, self.password, self.replace = (
files, reponame, version, username, password, replace)
self.current_tag_name = 'v' + self.version
@ -201,7 +201,7 @@ class GitHub(Base): # {{{
s.auth = (self.username, self.password)
s.headers.update({'Accept': 'application/vnd.github.v3+json'})
def __call__(self):
def __call__(self) -> None:
releases = self.releases()
# self.clean_older_releases(releases)
release = self.create_release(releases)
@ -239,7 +239,7 @@ class GitHub(Base): # {{{
if r.status_code != 200:
self.fail(r, 'Failed to set label for %s' % fname)
def clean_older_releases(self, releases):
def clean_older_releases(self, releases: Iterable[Dict[str, Any]]) -> None:
for release in releases:
if release.get(
'assets',
@ -256,7 +256,7 @@ class GitHub(Base): # {{{
'Failed to delete obsolete asset: %s for release: %s'
% (asset['name'], release['tag_name']))
def do_upload(self, url, path, desc, fname):
def do_upload(self, url: str, path: str, desc: str, fname: str) -> requests.Response:
mime_type = mimetypes.guess_type(fname)[0]
self.info('Uploading to GitHub: %s (%s)' % (fname, mime_type))
with ReadFileWithProgressReporting(path) as f:
@ -269,17 +269,17 @@ class GitHub(Base): # {{{
params={'name': fname},
data=cast(IO[bytes], f))
def fail(self, r, msg):
def fail(self, r: requests.Response, msg: str) -> None:
print(msg, ' Status Code: %s' % r.status_code, file=sys.stderr)
print("JSON from response:", file=sys.stderr)
pprint.pprint(dict(r.json()), stream=sys.stderr)
raise SystemExit(1)
def already_exists(self, r):
def already_exists(self, r: requests.Response) -> bool:
error_code = r.json().get('errors', [{}])[0].get('code', None)
return error_code == 'already_exists'
return bool(error_code == 'already_exists')
def existing_assets(self, release_id):
def existing_assets(self, release_id: str) -> Dict[str, str]:
url = self.API + 'repos/%s/%s/releases/%s/assets' % (
self.username, self.reponame, release_id)
r = self.requests.get(url)
@ -287,22 +287,21 @@ class GitHub(Base): # {{{
self.fail(r, 'Failed to get assets for release')
return {asset['name']: asset['id'] for asset in r.json()}
def releases(self):
def releases(self) -> List[Dict[str, Any]]:
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame
)
r = self.requests.get(url)
if r.status_code != 200:
self.fail(r, 'Failed to list releases')
return r.json()
return list(r.json())
def create_release(self, releases):
def create_release(self, releases: Iterable[Dict[str, str]]) -> Dict[str, Any]:
' Create a release on GitHub or if it already exists, return the existing release '
for release in releases:
# Check for existing release
if release['tag_name'] == self.current_tag_name:
return release
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame
)
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame)
r = self.requests.post(
url,
data=json.dumps({
@ -316,19 +315,17 @@ class GitHub(Base): # {{{
if r.status_code != 201:
self.fail(r, 'Failed to create release for version: %s' %
self.version)
return r.json()
return dict(r.json())
# }}}
def get_github_data():
def get_github_data() -> Dict[str, str]:
with open(os.environ['PENV'] + '/github-token') as f:
un, pw = f.read().strip().split(':')
return {'username': un, 'password': pw}
def run_upload(args):
def run_upload(args: Any) -> None:
files = {
os.path.join(build_path, 'build', f.format(version)): desc
for f, desc in {
@ -346,18 +343,18 @@ def run_upload(args):
gh()
def require_git_master(branch='master'):
def require_git_master(branch: str = 'master') -> None:
b = subprocess.check_output(['git', 'symbolic-ref', '--short', 'HEAD']).decode('utf-8').strip()
if b != branch:
raise SystemExit('You must be in the {} git branch'.format(branch))
def require_penv():
def require_penv() -> None:
if 'PENV' not in os.environ:
raise SystemExit('The PENV env var is not present, required for uploading releases')
def main():
def main() -> None:
require_git_master()
require_penv()
parser = argparse.ArgumentParser(description='Publish kitty')

232
setup.py
View File

@ -14,20 +14,20 @@ import subprocess
import sys
import sysconfig
import time
from collections import namedtuple
from contextlib import suppress
from functools import partial
from pathlib import Path
from typing import (
Callable, Dict, Iterator, List, NamedTuple, Optional, Sequence, Tuple,
Union
Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional,
Sequence, Set, Tuple, Union
)
from glfw import glfw # noqa
if sys.version_info[:2] < (3, 6):
raise SystemExit('kitty requires python >= 3.6')
base = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base)
from glfw import glfw # noqa
del sys.path[0]
verbose = False
@ -46,25 +46,28 @@ version = tuple(
)
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
class Env:
def __init__(
self, cc: str = '', cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
ldpaths: Optional[List[str]] = None, ccver: Optional[Tuple[int, int]] = None
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.ldpaths = cc, cppflags, cflags, ldflags, [] if ldpaths is None else ldpaths
self.ccver = ccver
def copy(self):
return Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), list(self.ldpaths), self.ccver)
Env = glfw.Env
env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
CompileKey = namedtuple('CompileKey', 'src dest')
class Options(argparse.Namespace):
action: str = 'build'
debug: bool = False
verbose: int = 0
sanitize: bool = False
prefix: str = './linux-package'
incremental: bool = True
profile: bool = False
for_freeze: bool = False
libdir_name: str = 'lib'
extra_logging: List[str] = []
update_check_interval: float = 24
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
@ -76,19 +79,19 @@ class Command(NamedTuple):
keyfile: Optional[str]
def emphasis(text):
def emphasis(text: str) -> str:
if sys.stdout.isatty():
text = '\033[32m' + text + '\033[39m'
return text
def error(text):
def error(text: str) -> str:
if sys.stdout.isatty():
text = '\033[91m' + text + '\033[39m'
return text
def pkg_config(pkg, *args):
def pkg_config(pkg: str, *args: str) -> List[str]:
try:
return list(
filter(
@ -103,7 +106,7 @@ def pkg_config(pkg, *args):
raise SystemExit('The package {} was not found on your system'.format(error(pkg)))
def at_least_version(package, major, minor=0):
def at_least_version(package: str, major: int, minor: int = 0) -> None:
q = '{}.{}'.format(major, minor)
if subprocess.run([PKGCONFIG, package, '--atleast-version=' + q]
).returncode != 0:
@ -146,15 +149,19 @@ def cc_version() -> Tuple[str, Tuple[int, int]]:
return cc, ver
def get_python_include_paths():
def get_python_include_paths() -> List[str]:
ans = []
for name in sysconfig.get_path_names():
if 'include' in name:
ans.append(name)
return sorted(frozenset(map(sysconfig.get_path, sorted(ans))))
def gp(x: str) -> Optional[str]:
return sysconfig.get_path(x)
return sorted(frozenset(filter(None, map(gp, sorted(ans)))))
def get_python_flags(cflags):
def get_python_flags(cflags: List[str]) -> List[str]:
cflags.extend('-I' + x for x in get_python_include_paths())
libs: List[str] = []
libs += (sysconfig.get_config_var('LIBS') or '').split()
@ -186,7 +193,7 @@ def get_python_flags(cflags):
return libs
def get_sanitize_args(cc, ccver):
def get_sanitize_args(cc: str, ccver: Tuple[int, int]) -> List[str]:
sanitize_args = ['-fsanitize=address']
if ccver >= (5, 0):
sanitize_args.append('-fsanitize=undefined')
@ -209,7 +216,7 @@ def test_compile(cc: str, *cflags: str, src: Optional[str] = None) -> bool:
return p.wait() == 0
def first_successful_compile(cc, *cflags, src=None):
def first_successful_compile(cc: str, *cflags: str, src: Optional[str] = None) -> str:
for x in cflags:
if test_compile(cc, *shlex.split(x), src=src):
return x
@ -217,9 +224,12 @@ def first_successful_compile(cc, *cflags, src=None):
def init_env(
debug=False, sanitize=False, native_optimizations=True, profile=False,
extra_logging=()
):
debug: bool = False,
sanitize: bool = False,
native_optimizations: bool = True,
profile: bool = False,
extra_logging: Iterable[str] = ()
) -> Env:
native_optimizations = native_optimizations and not sanitize and not debug
cc, ccver = cc_version()
print('CC:', cc, ccver)
@ -279,7 +289,7 @@ def init_env(
return Env(cc, cppflags, cflags, ldflags, ccver=ccver)
def kitty_env():
def kitty_env() -> Env:
ans = env.copy()
cflags = ans.cflags
cflags.append('-pthread')
@ -318,11 +328,11 @@ def kitty_env():
return ans
def define(x):
def define(x: str) -> str:
return '-D' + x
def run_tool(cmd, desc=None):
def run_tool(cmd: Union[str, List[str]], desc: Optional[str] = None) -> None:
if isinstance(cmd, str):
cmd = shlex.split(cmd[0])
if verbose:
@ -336,7 +346,7 @@ def run_tool(cmd, desc=None):
raise SystemExit(ret)
def get_vcs_rev_defines():
def get_vcs_rev_defines() -> List[str]:
ans = []
if os.path.exists('.git'):
try:
@ -355,13 +365,13 @@ def get_vcs_rev_defines():
return ans
SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[], Iterator[str]]]]] = {
SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[], Union[List[str], Iterator[str]]]]]] = {
'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
'kitty/data-types.c': ('kitty/data-types.c', get_vcs_rev_defines),
}
def newer(dest, *sources):
def newer(dest: str, *sources: str) -> bool:
try:
dtime = os.path.getmtime(dest)
except OSError:
@ -373,7 +383,7 @@ def newer(dest, *sources):
return False
def dependecies_for(src, obj, all_headers):
def dependecies_for(src: str, obj: str, all_headers: Iterable[str]) -> Iterable[str]:
dep_file = obj.rpartition('.')[0] + '.d'
try:
with open(dep_file) as f:
@ -398,7 +408,7 @@ def dependecies_for(src, obj, all_headers):
yield path
def parallel_run(items):
def parallel_run(items: List[Command]) -> None:
try:
num_workers = max(2, os.cpu_count() or 1)
except Exception:
@ -408,7 +418,7 @@ def parallel_run(items):
failed = None
num, total = 0, len(items)
def wait():
def wait() -> None:
nonlocal failed
if not workers:
return
@ -441,22 +451,33 @@ def parallel_run(items):
print(' done')
if failed:
print(failed.desc)
run_tool(failed.cmd)
run_tool(list(failed.cmd))
class CompilationDatabase:
def __init__(self, incremental):
def __init__(self, incremental: bool):
self.incremental = incremental
self.compile_commands = []
self.link_commands = []
self.compile_commands: List[Command] = []
self.link_commands: List[Command] = []
def add_command(
self,
desc: str,
cmd: List[str],
is_newer_func: Callable,
key: Optional[CompileKey] = None,
on_success: Optional[Callable] = None,
keyfile: Optional[str] = None
) -> None:
def no_op() -> None:
pass
def add_command(self, desc, cmd, is_newer_func, key=None, on_success=None, keyfile=None):
queue = self.link_commands if keyfile is None else self.compile_commands
queue.append(Command(desc, cmd, is_newer_func, on_success, key, keyfile))
queue.append(Command(desc, cmd, is_newer_func, on_success or no_op, key, keyfile))
def build_all(self):
def sort_key(compile_cmd):
def build_all(self) -> None:
def sort_key(compile_cmd: Command) -> int:
if compile_cmd.keyfile:
return os.path.getsize(compile_cmd.keyfile)
return 0
@ -474,12 +495,12 @@ class CompilationDatabase:
items.append(compile_cmd)
parallel_run(items)
def cmd_changed(self, compile_cmd):
def cmd_changed(self, compile_cmd: Command) -> bool:
key, cmd = compile_cmd.key, compile_cmd.cmd
return self.db.get(key) != cmd
return bool(self.db.get(key) != cmd)
def __enter__(self):
self.all_keys = set()
def __enter__(self) -> 'CompilationDatabase':
self.all_keys: Set[CompileKey] = set()
self.dbpath = os.path.abspath('compile_commands.json')
self.linkdbpath = os.path.join(os.path.dirname(self.dbpath), 'link_commands.json')
try:
@ -499,12 +520,12 @@ class CompilationDatabase:
self.linkdb = {k['output']: k['arguments'] for k in link_database}
return self
def __exit__(self, *a):
def __exit__(self, *a: object) -> None:
cdb = self.db
for key in set(cdb) - self.all_keys:
del cdb[key]
compilation_database = [
{'file': c.key.src, 'arguments': c.cmd, 'directory': base, 'output': c.key.dest} for c in self.compile_commands
{'file': c.key.src, 'arguments': c.cmd, 'directory': base, 'output': c.key.dest} for c in self.compile_commands if c.key is not None
]
with open(self.dbpath, 'w') as f:
json.dump(compilation_database, f, indent=2, sort_keys=True)
@ -512,7 +533,14 @@ class CompilationDatabase:
json.dump([{'output': c.key, 'arguments': c.cmd, 'directory': base} for c in self.link_commands], f, indent=2, sort_keys=True)
def compile_c_extension(kenv, module, compilation_database, sources, headers, desc_prefix=''):
def compile_c_extension(
kenv: Env,
module: str,
compilation_database: CompilationDatabase,
sources: List[str],
headers: List[str],
desc_prefix: str = ''
) -> None:
prefix = os.path.basename(module)
objects = [
os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o')
@ -547,13 +575,13 @@ def compile_c_extension(kenv, module, compilation_database, sources, headers, de
linker_cflags = list(filter(lambda x: x not in unsafe, kenv.cflags))
cmd = [kenv.cc] + linker_cflags + kenv.ldflags + objects + kenv.ldpaths + ['-o', dest]
def on_success():
def on_success() -> None:
os.rename(dest, real_dest)
compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=module + '.so')
compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=CompileKey('', module + '.so'))
def find_c_files():
def find_c_files() -> Tuple[List[str], List[str]]:
ans, headers = [], []
d = 'kitty'
exclude = {'fontconfig.c', 'freetype.c', 'desktop.c'} if is_macos else {'core_text.m', 'cocoa_window.m', 'macos_process_info.c'}
@ -564,10 +592,10 @@ def find_c_files():
elif ext == '.h':
headers.append(os.path.join('kitty', x))
ans.append('kitty/parser_dump.c')
return tuple(ans), tuple(headers)
return ans, headers
def compile_glfw(compilation_database):
def compile_glfw(compilation_database: CompilationDatabase) -> None:
modules = 'cocoa' if is_macos else 'x11 wayland'
for module in modules.split():
try:
@ -592,7 +620,7 @@ def compile_glfw(compilation_database):
sources, all_headers, desc_prefix='[{}] '.format(module))
def kittens_env():
def kittens_env() -> Env:
kenv = env.copy()
cflags = kenv.cflags
cflags.append('-pthread')
@ -602,10 +630,10 @@ def kittens_env():
return kenv
def compile_kittens(compilation_database):
def compile_kittens(compilation_database: CompilationDatabase) -> None:
kenv = kittens_env()
def list_files(q):
def list_files(q: str) -> List[str]:
return sorted(glob.glob(q))
def files(
@ -632,21 +660,22 @@ def compile_kittens(compilation_database):
kenv, dest, compilation_database, sources, all_headers + ['kitty/data-types.h'])
def build(args, native_optimizations=True):
def build(args: Options, native_optimizations: bool = True) -> None:
global env
env = init_env(args.debug, args.sanitize, native_optimizations, args.profile, args.extra_logging)
sources, headers = find_c_files()
compile_c_extension(
kitty_env(), 'kitty/fast_data_types', args.compilation_database, *find_c_files()
kitty_env(), 'kitty/fast_data_types', args.compilation_database, sources, headers
)
compile_glfw(args.compilation_database)
compile_kittens(args.compilation_database)
def safe_makedirs(path):
def safe_makedirs(path: str) -> None:
os.makedirs(path, exist_ok=True)
def build_launcher(args, launcher_dir='.', bundle_type='source'):
def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> None:
cflags = '-Wall -Werror -fpie'.split()
cppflags = []
libs: List[str] = []
@ -696,7 +725,7 @@ def build_launcher(args, launcher_dir='.', bundle_type='source'):
# Packaging {{{
def copy_man_pages(ddir):
def copy_man_pages(ddir: str) -> None:
mandir = os.path.join(ddir, 'share', 'man')
safe_makedirs(mandir)
with suppress(FileNotFoundError):
@ -711,7 +740,7 @@ make && make docs
shutil.copytree(src, os.path.join(mandir, 'man1'))
def copy_html_docs(ddir):
def copy_html_docs(ddir: str) -> None:
htmldir = os.path.join(ddir, 'share', 'doc', appname, 'html')
safe_makedirs(os.path.dirname(htmldir))
with suppress(FileNotFoundError):
@ -726,7 +755,7 @@ make && make docs
shutil.copytree(src, htmldir)
def compile_python(base_path):
def compile_python(base_path: str) -> None:
import compileall
import py_compile
try:
@ -738,18 +767,18 @@ def compile_python(base_path):
if f.rpartition('.')[-1] in ('pyc', 'pyo'):
os.remove(os.path.join(root, f))
def c(base_path: str, **kw) -> None:
def c(base_path: str, **kw: object) -> None:
try:
kw['invalidation_mode'] = py_compile.PycInvalidationMode.UNCHECKED_HASH
except AttributeError:
pass
compileall.compile_dir(base_path, **kw)
compileall.compile_dir(base_path, **kw) # type: ignore
for optimize in (0, 1, 2):
c(base_path, ddir='', force=True, optimize=optimize, quiet=1, workers=num_workers)
def create_linux_bundle_gunk(ddir, libdir_name):
def create_linux_bundle_gunk(ddir: str, libdir_name: str) -> None:
if not os.path.exists('docs/_build/html'):
run_tool(['make', 'docs'])
copy_man_pages(ddir)
@ -774,16 +803,17 @@ Icon=kitty
Categories=System;TerminalEmulator;
'''
)
ddir = Path(ddir)
in_src_launcher = ddir / (libdir_name + '/kitty/kitty/launcher/kitty')
launcher = ddir / 'bin/kitty'
base = Path(ddir)
in_src_launcher = base / (libdir_name + '/kitty/kitty/launcher/kitty')
launcher = base / 'bin/kitty'
if os.path.exists(in_src_launcher):
os.remove(in_src_launcher)
os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
os.symlink(os.path.relpath(launcher, os.path.dirname(in_src_launcher)), in_src_launcher)
def macos_info_plist():
def macos_info_plist() -> bytes:
import plistlib
VERSION = '.'.join(map(str, version))
pl = dict(
@ -827,7 +857,7 @@ def macos_info_plist():
return plistlib.dumps(pl)
def create_macos_app_icon(where='Resources'):
def create_macos_app_icon(where: str = 'Resources') -> None:
logo_dir = os.path.abspath(os.path.join('logo', appname + '.iconset'))
subprocess.check_call([
'iconutil', '-c', 'icns', logo_dir, '-o',
@ -835,7 +865,7 @@ def create_macos_app_icon(where='Resources'):
])
def create_minimal_macos_bundle(args, where):
def create_minimal_macos_bundle(args: Options, where: str) -> None:
if os.path.exists(where):
shutil.rmtree(where)
bin_dir = os.path.join(where, 'kitty.app/Contents/MacOS')
@ -851,8 +881,8 @@ def create_minimal_macos_bundle(args, where):
create_macos_app_icon(resources_dir)
def create_macos_bundle_gunk(ddir):
ddir = Path(ddir)
def create_macos_bundle_gunk(dest: str) -> None:
ddir = Path(dest)
os.mkdir(ddir / 'Contents')
with open(ddir / 'Contents/Info.plist', 'wb') as fp:
fp.write(macos_info_plist())
@ -869,7 +899,7 @@ def create_macos_bundle_gunk(ddir):
create_macos_app_icon(os.path.join(ddir, 'Contents', 'Resources'))
def package(args, bundle_type):
def package(args: Options, bundle_type: str) -> None:
ddir = args.prefix
if bundle_type == 'linux-freeze':
args.libdir_name = 'lib'
@ -891,7 +921,7 @@ def package(args, bundle_type):
shutil.copy2('logo/beam-cursor.png', os.path.join(libdir, 'logo'))
shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
def src_ignore(parent, entries):
def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
return [
x for x in entries
if '.' in x and x.rpartition('.')[2] not in
@ -920,9 +950,9 @@ def package(args, bundle_type):
# }}}
def clean():
def clean() -> None:
def safe_remove(*entries):
def safe_remove(*entries: str) -> None:
for x in entries:
if os.path.exists(x):
if os.path.isdir(x):
@ -949,66 +979,66 @@ def clean():
os.unlink(x)
def option_parser(): # {{{
def option_parser() -> argparse.ArgumentParser: # {{{
p = argparse.ArgumentParser()
p.add_argument(
'action',
nargs='?',
default='build',
default=Options.action,
choices='build test linux-package kitty.app linux-freeze macos-freeze clean'.split(),
help='Action to perform (default is build)'
)
p.add_argument(
'--debug',
default=False,
default=Options.debug,
action='store_true',
help='Build extension modules with debugging symbols'
)
p.add_argument(
'-v', '--verbose',
default=0,
default=Options.verbose,
action='count',
help='Be verbose'
)
p.add_argument(
'--sanitize',
default=False,
default=Options.sanitize,
action='store_true',
help='Turn on sanitization to detect memory access errors and undefined behavior. This is a big performance hit.'
)
p.add_argument(
'--prefix',
default='./linux-package',
default=Options.prefix,
help='Where to create the linux package'
)
p.add_argument(
'--full',
dest='incremental',
default=True,
default=Options.incremental,
action='store_false',
help='Do a full build, even for unchanged files'
)
p.add_argument(
'--profile',
default=False,
default=Options.profile,
action='store_true',
help='Use the -pg compile flag to add profiling information'
)
p.add_argument(
'--for-freeze',
default=False,
default=Options.for_freeze,
action='store_true',
help='Internal use'
)
p.add_argument(
'--libdir-name',
default='lib',
default=Options.libdir_name,
help='The name of the directory inside --prefix in which to store compiled files. Defaults to "lib"'
)
p.add_argument(
'--extra-logging',
action='append',
default=[],
default=Options.extra_logging,
choices=('event-loop',),
help='Turn on extra logging for debugging in this build. Can be specified multiple times, to turn'
' on different types of logging.'
@ -1016,7 +1046,7 @@ def option_parser(): # {{{
p.add_argument(
'--update-check-interval',
type=float,
default=24,
default=Options.update_check_interval,
help='When building a package, the default value for the update_check_interval setting will'
' be set to this number. Use zero to disable update checking.'
)
@ -1024,11 +1054,11 @@ def option_parser(): # {{{
# }}}
def main():
def main() -> None:
global verbose
if sys.version_info < (3, 5):
raise SystemExit('python >= 3.5 required')
args = option_parser().parse_args()
args = option_parser().parse_args(namespace=Options())
verbose = args.verbose > 0
args.prefix = os.path.abspath(args.prefix)
os.chdir(base)