#!/usr/bin/env python3 #vim: set filetype=python import logging import json import enum import pathlib import sys import argparse #import optparse import dataclasses import subprocess import os from typing import ( Optional, Any, TypeAlias, Literal, cast, BinaryIO, Generator, ClassVar, ) logger = logging.getLogger() def js(argv: list[str]) -> int: return subprocess.check_call([ 'sudo', 'docker-compose', '--project-directory', os.path.abspath( os.path.dirname(__file__), ), '-f', os.path.abspath( os.path.join( os.path.dirname(__file__), 'docker', 'js', 'docker-compose.yml', ) ), *argv, ]) def env( argv: Optional[list[str]] = None, **kwargs: Any, ) -> Optional[subprocess.CompletedProcess[bytes]]: env_path = pathlib.Path(__file__).parent / 'tmp' / 'env3' if not env_path.exists(): subprocess.check_call([ sys.executable, '-m', 'venv', '--system-site-packages', str(env_path) ]) subprocess.check_call([ env_path / 'bin' / 'python3', '-m', 'pip', 'install', '-r', 'requirements.txt', ]) if not argv is None: return subprocess.run([ str(env_path / 'bin' / 'python3'), *argv, ], **kwargs) return None def ruff(argv: list[str]) -> None: parser = argparse.ArgumentParser() parser.add_argument( '-i', dest='paths', help='specify paths to check', default=[], action='append', ) parser.add_argument( '-e', dest='exclude', help='rules to ignore', default=[], action='append', ) options, args = parser.parse_known_args(argv) if len(options.paths) == 0: options.paths.extend([ '.', 'dotfiles/.local/bin/commands', ]) if len(options.exclude) == 0: options.exclude.extend([ 'E731', 'E713', 'E714', 'E703', ]) res = env([ '-m', 'ruff', 'check', *args, '--output-format', 'json', '--ignore', ','.join(options.exclude), *options.paths, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) assert not res is None errors = json.loads(res.stdout.decode('utf-8')) g: dict[str, Any] = dict() for o in errors: if not o['filename'] in g: g[o['filename']] = [] g[o['filename']].append(o) h = { k : len(v) for k, v in g.items() } logger.info(json.dumps(errors, indent=4)) logger.info(json.dumps(h, indent=4)) @dataclasses.dataclass class MypyFormatEntry: name : str value : str def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): raise NotImplementedError return self.value == other.value class MypyFormat: vscode : ClassVar[MypyFormatEntry] = MypyFormatEntry(name='vscode', value='vscode') json : ClassVar[MypyFormatEntry] = MypyFormatEntry(name='json', value='json') @classmethod def from_value(cls, value: str) -> MypyFormatEntry: for e in cls.entries(): if value == e.value: return e raise NotImplementedError @classmethod def entries(cls) -> Generator[MypyFormatEntry, None, None,]: for o in dir(cls): e = getattr(cls, o) if not isinstance(e, MypyFormatEntry): continue yield e def mypy(argv: list[str]) -> None: parser = argparse.ArgumentParser() parser.add_argument( '-i', dest='paths', help='specify paths to check', default=[], action='append', ) parser.add_argument( '-f', '--format', dest='_format', help='output format of errors', default=MypyFormat.json.value, choices=[ o.value for o in MypyFormat.entries() ], ) options, args = parser.parse_known_args(argv) options.format = MypyFormat.from_value(options._format) if len(options.paths) == 0: options.paths.extend([ 'dotfiles/.local/bin/commands', 'python', 'm.py', ]) res = env([ '-m', 'mypy', '--strict', '-O', 'json', *args, *options.paths, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) assert not res is None try: errors = [ json.loads(o) for o in res.stdout.decode('utf-8').splitlines() if not o.strip() == '' ] except: logger.exception('') logger.error(res.stdout.decode('utf-8')) logger.error(res.stderr.decode('utf-8')) sys.exit(res.returncode) g : dict[str, Any] = dict() for o in errors: if not o['file'] in g: g[o['file']] = [] g[o['file']].append(o) h = { k : len(v) for k, v in g.items() } if options.format == MypyFormat.vscode: for o in errors: sys.stdout.write('[%s] %s:%d,%d %s - %s - %s\n' % ( o['severity'], o['file'], o['line'], o['column'], o['message'], o['hint'], o['code'], )) sys.stdout.flush() #logger.info(json.dumps(errors, indent=4)) logger.info(json.dumps(h, indent=4)) else: logger.info(json.dumps(errors, indent=4)) logger.info(json.dumps(h, indent=4)) def inside_env() -> bool: try: import numpy return True except Exception: return False #class Commands(enum.StrEnum): # js = 'js' # mypy = 'mypy' # env = 'env' # ruff = 'ruff' # m2 = 'm2' Command_args = ['js', 'mypy', 'env', 'ruff', 'm2'] Command : TypeAlias = Literal['js', 'mypy', 'env', 'ruff', 'm2'] def run(argv: Optional[list[str]] = None) -> None: logging.basicConfig( level=logging.INFO, format=( '%(levelname)s:%(name)s:%(message)s' ':%(process)d' ':%(asctime)s' ':%(pathname)s:%(funcName)s:%(lineno)s' ), ) if argv is None: argv = sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument( 'command', #'_command', choices=[ o for o in Command_args ], #required=True, ) options, args = parser.parse_known_args(argv) assert options.command in Command_args #options.command = Commands(options._command) if options.command == 'js': js(args) elif options.command == 'env': env(args) elif options.command == 'mypy': mypy(args) elif options.command == 'ruff': ruff(args) elif options.command == 'm2': if not inside_env(): env(['--', '_m.py', 'm2', *args]) return import python.tasks.cython python.tasks.cython.mypyc_build( pathlib.Path('_m.py') ) else: raise NotImplementedError if __name__ == '__main__': run(sys.argv[1:])