Compare commits

...

22 Commits

Author SHA1 Message Date
skullY 30c89ec8e0 New release: 0.0.33 → 0.0.34 2020-04-29 10:39:57 -07:00
Zach White a7a7bd9f08 improve the qmk setup flow (#20)
* improve the qmk setup flow

* Apply suggestions from code review

Co-Authored-By: Erovia <Erovia@users.noreply.github.com>

* Update qmk_cli/subcommands/setup.py

* explicity set encoding to utf-8

* polishing

Co-authored-by: Erovia <Erovia@users.noreply.github.com>
2020-04-29 10:38:56 -07:00
skullY 5229ce14e3 New release: 0.0.32 → 0.0.33 2020-04-15 13:53:35 -07:00
Erovia 8fa960f25e Merge pull request #19 from qmk/fix_store_true_false 2020-04-15 22:49:17 +02:00
skullY 1481dfe157 Make arg_only apply only to the subcommand it is part of 2020-04-15 12:07:15 -07:00
skullY ac39b9bb0f Fix setting config values for store_true and store_false 2020-04-15 09:30:49 -07:00
skullY 44a446308f New release: 0.0.31 → 0.0.32 2020-04-11 10:59:02 -07:00
Erovia 90bdf52416 Merge pull request #18 from Erovia/cli/win_env_check
Add warning message for MSYS users
2020-04-11 19:56:04 +02:00
Erovia 191980052e Add warning message for MSYS users 2020-04-11 19:44:56 +02:00
skullydazed 5fbdd95c4b Fix indentation 2020-04-11 10:31:54 -07:00
skullydazed 1002360e20 Update qmk_cli/script_qmk.py
Co-Authored-By: Erovia <Erovia@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
Joel Challis a03aa1cbba Update qmk_cli/subcommands/setup.py
Co-Authored-By: skullydazed <skullydazed@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
zvecr a0b692d718 Handle failed import with exception instead 2020-04-11 10:31:54 -07:00
zvecr ab7d0c6e46 Protect against empty QMK_HOME folder 2020-04-11 10:31:54 -07:00
skullY 73c903b8b6 Add --no flag 2020-04-11 09:57:31 -07:00
skullydazed 8f2185af32 Update qmk_cli/subcommands/setup.py 2020-04-11 09:57:31 -07:00
Erovia bae2f1f1ee Fix argument passing to doctor subcommand
When 'qmk setup' is started with the '-y' argument, this argument should be specified for 'qmk doctor' as well.
2020-04-11 09:57:31 -07:00
Zach White 120ce557f1 New release: 0.0.30 → 0.0.31 2020-03-26 07:51:42 -07:00
Erovia a15b0d6460 Fix WinError 193 on MSYS2 2020-03-26 07:51:17 -07:00
skullY f0dafaf068 New release: 0.0.29 → 0.0.30 2020-03-10 11:37:44 -07:00
skullY 3414bdcaa3 add nose2 to requirements 2020-03-10 11:37:31 -07:00
Mikkel Jeppesen c8567d8098 Made bade in the readme nicer 2020-02-25 14:33:37 -08:00
10 changed files with 186 additions and 117 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# QMK CLI # QMK CLI
![CLI Setup](https://github.com/qmk/qmk_cli/workflows/CLI%20Setup/badge.svg) [![CLI Setup](https://github.com/qmk/qmk_cli/workflows/CLI%20Setup/badge.svg)](https://github.com/qmk/qmk_cli/actions?query=workflow%3A%22CLI+Setup%22)
A program to help users work with [QMK Firmware](https://qmk.fm/). A program to help users work with [QMK Firmware](https://qmk.fm/).
# Features # Features
+124 -81
View File
@@ -242,15 +242,24 @@ class SubparserWrapper(object):
This also stores the default for the argument in `self.cli.default_arguments`. This also stores the default for the argument in `self.cli.default_arguments`.
""" """
if 'action' in kwargs and kwargs['action'] == 'store_boolean': if kwargs.get('action') == 'store_boolean':
# Store boolean will call us again with the enable/disable flag arguments # Store boolean will call us again with the enable/disable flag arguments
return handle_store_boolean(self, *args, **kwargs) return handle_store_boolean(self, *args, **kwargs)
self.cli.acquire_lock() self.cli.acquire_lock()
argument_name = self.cli.get_argument_name(*args, **kwargs)
self.subparser.add_argument(*args, **kwargs) self.subparser.add_argument(*args, **kwargs)
if kwargs.get('action') == 'store_false':
self.cli._config_store_false.append(argument_name)
if kwargs.get('action') == 'store_true':
self.cli._config_store_true.append(argument_name)
if self.submodule not in self.cli.default_arguments: if self.submodule not in self.cli.default_arguments:
self.cli.default_arguments[self.submodule] = {} self.cli.default_arguments[self.submodule] = {}
self.cli.default_arguments[self.submodule][self.cli.get_argument_name(*args, **kwargs)] = kwargs.get('default') self.cli.default_arguments[self.submodule][argument_name] = kwargs.get('default')
self.cli.release_lock() self.cli.release_lock()
@@ -268,11 +277,13 @@ class MILC(object):
# Define some basic info # Define some basic info
self.acquire_lock() self.acquire_lock()
self._config_store_true = []
self._config_store_false = []
self._description = None self._description = None
self._entrypoint = None self._entrypoint = None
self._inside_context_manager = False self._inside_context_manager = False
self.ansi = ansi_colors self.ansi = ansi_colors
self.arg_only = [] self.arg_only = {}
self.config = self.config_source = None self.config = self.config_source = None
self.config_file = None self.config_file = None
self.default_arguments = {} self.default_arguments = {}
@@ -284,7 +295,7 @@ class MILC(object):
self.prog_name = self.prog_name.split('/')[-1] self.prog_name = self.prog_name.split('/')[-1]
# Initialize all the things # Initialize all the things
self.read_config_file() self.initialize_config()
self.initialize_argparse() self.initialize_argparse()
self.initialize_logging() self.initialize_logging()
@@ -377,7 +388,7 @@ class MILC(object):
self.add_argument('--log-file', help='File to write log messages to') self.add_argument('--log-file', help='File to write log messages to')
self.add_argument('--color', action='store_boolean', default=True, help='color in output') self.add_argument('--color', action='store_boolean', default=True, help='color in output')
self.add_argument('--config-file', help='The location for the configuration file') self.add_argument('--config-file', help='The location for the configuration file')
self.arg_only.append('config_file') self.arg_only['config_file'] = ['general']
def add_subparsers(self, title='Sub-commands', **kwargs): def add_subparsers(self, title='Sub-commands', **kwargs):
if self._inside_context_manager: if self._inside_context_manager:
@@ -427,17 +438,20 @@ class MILC(object):
raise RuntimeError('You must run this before the with statement!') raise RuntimeError('You must run this before the with statement!')
def argument_function(handler): def argument_function(handler):
if 'arg_only' in kwargs and kwargs['arg_only']: subcommand_name = handler.__name__.replace("_", "-")
if kwargs.get('arg_only'):
arg_name = self.get_argument_name(*args, **kwargs) arg_name = self.get_argument_name(*args, **kwargs)
self.arg_only.append(arg_name) if arg_name not in self.arg_only:
self.arg_only[arg_name] = []
self.arg_only[arg_name].append(subcommand_name)
del kwargs['arg_only'] del kwargs['arg_only']
name = handler.__name__.replace("_", "-")
if handler is self._entrypoint: if handler is self._entrypoint:
self.add_argument(*args, **kwargs) self.add_argument(*args, **kwargs)
elif name in self.subcommands: elif subcommand_name in self.subcommands:
self.subcommands[name].add_argument(*args, **kwargs) self.subcommands[subcommand_name].add_argument(*args, **kwargs)
else: else:
raise RuntimeError('Decorated function is not entrypoint or subcommand!') raise RuntimeError('Decorated function is not entrypoint or subcommand!')
@@ -468,22 +482,20 @@ class MILC(object):
self.release_lock() self.release_lock()
def read_config_file(self): def read_config_file(self, config_file):
"""Read in the configuration file and store it in self.config. """Read in the configuration file and return Configuration objects for it and the config_source.
""" """
self.acquire_lock() config = Configuration()
self.config = Configuration() config_source = Configuration()
self.config_source = Configuration()
self.config_file = self.find_config_file()
if self.config_file and self.config_file.exists(): if config_file.exists():
config = RawConfigParser(self.config) raw_config = RawConfigParser()
config.read(str(self.config_file)) raw_config.read(str(config_file))
# Iterate over the config file options and write them into self.config # Iterate over the config file options and write them into config
for section in config.sections(): for section in raw_config.sections():
for option in config.options(section): for option in raw_config.options(section):
value = config.get(section, option) value = raw_config.get(section, option)
# Coerce values into useful datatypes # Coerce values into useful datatypes
if value.lower() in ['1', 'yes', 'true', 'on']: if value.lower() in ['1', 'yes', 'true', 'on']:
@@ -498,9 +510,17 @@ class MILC(object):
else: else:
value = int(value) value = int(value)
self.config[section][option] = value config[section][option] = value
self.config_source[section][option] = 'config_file' config_source[section][option] = 'config_file'
return config, config_source
def initialize_config(self):
"""Read in the configuration file and store it in self.config.
"""
self.acquire_lock()
self.config_file = self.find_config_file()
self.config, self.config_source = self.read_config_file(self.config_file)
self.release_lock() self.release_lock()
def merge_args_into_config(self): def merge_args_into_config(self):
@@ -511,38 +531,88 @@ class MILC(object):
if argument in ('subparsers', 'entrypoint'): if argument in ('subparsers', 'entrypoint'):
continue continue
if argument not in self.arg_only: # Find the argument's section
# Find the argument's section # Underscores in command's names are converted to dashes during initialization.
# Underscores in command's names are converted to dashes during initialization. # TODO(Erovia) Find a better solution
# TODO(Erovia) Find a better solution entrypoint_name = self._entrypoint.__name__.replace("_", "-")
entrypoint_name = self._entrypoint.__name__.replace("_", "-") if entrypoint_name in self.default_arguments and argument in self.default_arguments[entrypoint_name]:
if entrypoint_name in self.default_arguments and argument in self.default_arguments[entrypoint_name]: argument_found = True
argument_found = True section = self._entrypoint.__name__
section = self._entrypoint.__name__ if argument in self.default_arguments['general']:
if argument in self.default_arguments['general']: argument_found = True
argument_found = True section = 'general'
section = 'general'
if not argument_found: if not argument_found:
raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!') raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!')
exit(1) exit(1)
if argument not in self.arg_only or section not in self.arg_only[argument]:
# Determine the arg value and source
arg_value = getattr(self.args, argument)
if argument in self._config_store_true and arg_value:
passed_on_cmdline = True
elif argument in self._config_store_false and not arg_value:
passed_on_cmdline = True
elif arg_value is not None:
passed_on_cmdline = True
else:
passed_on_cmdline = False
# Merge this argument into self.config # Merge this argument into self.config
if argument in self.default_arguments['general'] or argument in self.default_arguments[entrypoint_name]: if passed_on_cmdline and (argument in self.default_arguments['general'] or argument in self.default_arguments[entrypoint_name] or argument not in self.config[entrypoint_name]):
arg_value = getattr(self.args, argument) self.config[section][argument] = arg_value
if arg_value is not None: self.config_source[section][argument] = 'argument'
self.config[section][argument] = arg_value
self.config_source[section][argument] = 'argument'
else:
if argument not in self.config[entrypoint_name]:
# Check if the argument exist for this section
arg = getattr(self.args, argument)
if arg is not None:
self.config[section][argument] = arg
self.config_source[section][argument] = 'argument'
self.release_lock() self.release_lock()
def _save_config_file(self, config):
"""Write config to disk.
"""
# Generate a sanitized version of our running configuration
sane_config = RawConfigParser()
for section_name, section in config._config.items():
sane_config.add_section(section_name)
for option_name, value in section.items():
if section_name == 'general':
if option_name in ['config_file']:
continue
if value is not None:
sane_config.set(section_name, option_name, str(value))
config_dir = self.config_file.parent
if not config_dir.exists():
config_dir.mkdir(parents=True, exist_ok=True)
# Write the config file atomically.
self.acquire_lock()
with NamedTemporaryFile(mode='w', dir=str(config_dir), delete=False) as tmpfile:
sane_config.write(tmpfile)
if os.path.getsize(tmpfile.name) > 0:
os.replace(tmpfile.name, str(self.config_file))
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name)
self.release_lock()
def write_config_option(self, section, option):
"""Save a single config option to the config file.
"""
if not self.config_file:
self.log.warning('%s.config_file not set, not saving config!', self.__class__.__name__)
return
config, config_source = self.read_config_file(self.config_file)
if section in config and option in config[section] and config[section][option] is None:
del(config[section][option])
else:
config[section][option] = str(self.config[section][option])
self._save_config_file(config)
# Housekeeping
self.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
def save_config(self): def save_config(self):
"""Save the current configuration to the config file. """Save the current configuration to the config file.
""" """
@@ -552,36 +622,9 @@ class MILC(object):
self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__) self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
return return
self.acquire_lock() # Write config to disk
self._save_config_file(self.config)
# Generate a sanitized version of our running configuration self.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
config = RawConfigParser()
for section_name, section in self.config._config.items():
config.add_section(section_name)
for option_name, value in section.items():
if section_name == 'general':
if option_name in ['config_file']:
continue
if value is not None:
config.set(section_name, option_name, str(value))
# Write out the config file
config_dir = self.config_file.parent
if not config_dir.exists():
config_dir.mkdir(parents=True, exist_ok=True)
with NamedTemporaryFile(mode='w', dir=str(config_dir), delete=False) as tmpfile:
config.write(tmpfile)
# Move the new config file into place atomically
if os.path.getsize(tmpfile.name) > 0:
os.replace(tmpfile.name, str(self.config_file))
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name)
# Housekeeping
self.release_lock()
cli.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
def __call__(self): def __call__(self):
"""Execute the entrypoint function. """Execute the entrypoint function.
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware.""" """A program to help you work with qmk_firmware."""
__version__ = '0.0.29' __version__ = '0.0.34'
+5 -5
View File
@@ -9,7 +9,7 @@ default_fork = 'qmk/' + default_repo
default_branch = 'master' default_branch = 'master'
def clone(url, destination, branch): def git_clone(url, destination, branch):
git_clone = [ git_clone = [
'git', 'git',
'clone', 'clone',
@@ -20,13 +20,13 @@ def clone(url, destination, branch):
] ]
cli.log.debug('Git clone command: %s', git_clone) cli.log.debug('Git clone command: %s', git_clone)
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as p: with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
for line in p.stdout: for line in p.stdout:
print(line, end='') print(line, end='')
if p.returncode != 0: if p.returncode != 0:
cli.log.error('git clone exited %d', p.returncode) cli.log.error('git clone exited %d', p.returncode)
return False
else: else:
cli.log.info('Successfully cloned %s to %s!', cli.args.fork, cli.args.destination) cli.log.info('Successfully cloned %s to %s!', url, destination)
return True
return p.returncode
+3 -2
View File
@@ -1,6 +1,6 @@
"""Useful helper functions. """Useful helper functions.
""" """
from milc import cli from milc import cli, format_ansi
def question(question, boolean=True, default=''): def question(question, boolean=True, default=''):
@@ -18,10 +18,11 @@ def question(question, boolean=True, default=''):
else: else:
answer_key = 'y/n' answer_key = 'y/n'
prompt = '*** %s [%s] ' % (question, answer_key) prompt = format_ansi('\n*** %s [%s] ' % (question, answer_key))
while True: while True:
answer = input(prompt) answer = input(prompt)
print()
if answer == '' and default.lower() == 'y': if answer == '' and default.lower() == 'y':
answer = 'y' answer = 'y'
elif answer == '' and default.lower() == 'n': elif answer == '' and default.lower() == 'n':
+13 -2
View File
@@ -9,6 +9,7 @@ import subprocess
import sys import sys
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
import platform
import milc import milc
@@ -30,7 +31,7 @@ def in_qmk_firmware():
while len(cur_dir.parents) > 0: while len(cur_dir.parents) > 0:
found_bin = cur_dir / 'bin' / 'qmk' found_bin = cur_dir / 'bin' / 'qmk'
if found_bin.is_file(): if found_bin.is_file():
command = [found_bin.as_posix(), '--version'] command = [sys.executable, found_bin.as_posix()]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0: if result.returncode == 0:
@@ -69,6 +70,11 @@ def main():
print('Warning: Your Python version is out of date! Some subcommands may not work!') print('Warning: Your Python version is out of date! Some subcommands may not work!')
print('Please upgrade to Python 3.6 or later.') print('Please upgrade to Python 3.6 or later.')
if 'windows' in platform.platform().lower():
if 'mingw64' not in sys.executable or 'mingw64' not in os.environ.get('MSYSTEM_PREFIX', ''):
print('Warning: It seems you are not using the MINGW64 terminal.')
print('While the MSYS one can work, too, we recommend/support you start "MSYS2 MinGW 64-bit".\n')
# Environment setup # Environment setup
import qmk_cli import qmk_cli
milc.cli.version = qmk_cli.__version__ milc.cli.version = qmk_cli.__version__
@@ -82,7 +88,12 @@ def main():
if qmk_firmware.exists(): if qmk_firmware.exists():
os.chdir(str(qmk_firmware)) os.chdir(str(qmk_firmware))
sys.path.append(str(qmk_firmware / 'lib' / 'python')) sys.path.append(str(qmk_firmware / 'lib' / 'python'))
import qmk.cli try:
import qmk.cli
except ImportError:
print('Error: %s is too old or not set up correctly!' % qmk_firmware)
print('Please update it or remove it completely before continuing.')
sys.exit(1)
# Call the entrypoint # Call the entrypoint
milc.cli() milc.cli()
+2 -3
View File
@@ -4,7 +4,7 @@ import os
from pathlib import Path from pathlib import Path
from milc import cli from milc import cli
from qmk_cli.git import clone as git_clone from qmk_cli.git import git_clone
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
@@ -27,5 +27,4 @@ def clone(cli):
cli.log.error('Destination already exists: %s', cli.args.destination) cli.log.error('Destination already exists: %s', cli.args.destination)
exit(1) exit(1)
success = git_clone(git_url, cli.args.destination, cli.config.clone.branch) return git_clone(git_url, cli.args.destination, cli.config.clone.branch)
exit(0 if success else 1)
+35 -21
View File
@@ -1,12 +1,13 @@
"""Setup qmk_firmware on your computer. """Setup qmk_firmware on your computer.
""" """
import os import os
import shlex
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from milc import cli from milc import cli
from qmk_cli.git import clone from qmk_cli.git import git_clone
from qmk_cli.helpers import question from qmk_cli.helpers import question
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
@@ -14,35 +15,48 @@ default_fork = 'qmk/' + default_repo
default_branch = 'master' default_branch = 'master'
@cli.argument('-y', '--yes', action='store_true', help='Answer yes to all questions') @cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions')
@cli.argument('-y', '--yes', arg_only=True, action='store_true', help='Answer yes to all questions')
@cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from') @cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from')
@cli.argument('-b', '--branch', default=default_branch, help='The branch to clone') @cli.argument('-b', '--branch', default=default_branch, help='The branch to clone')
@cli.argument('destination', default=os.environ['QMK_HOME'], nargs='?', help='The directory to clone to') @cli.argument('-H', '--home', default=Path(os.environ['QMK_HOME']), type=Path, help='The location for QMK Firmware. Defaults to %s' % (os.environ['QMK_HOME'],))
@cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone') @cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone')
@cli.subcommand('Setup your computer for qmk_firmware.') @cli.subcommand('Setup your computer for qmk_firmware.')
def setup(cli): def setup(cli):
qmk_firmware = Path(cli.args.destination) """Guide the user through setting up their QMK environment.
"""
clone_prompt = 'Would you like to clone {fg_cyan}%s{fg_reset} to {fg_cyan}%s{fg_reset}?' % (cli.args.fork, shlex.quote(str(cli.args.home)))
home_prompt = 'Would you like to set {fg_cyan}%s{fg_reset} as your QMK home?' % (shlex.quote(str(cli.args.home)),)
# Sanity checks
if cli.args.yes and cli.args.no:
cli.log.error("Can't use both --yes and --no at the same time.")
exit(1)
# Check on qmk_firmware, and if it doesn't exist offer to check it out. # Check on qmk_firmware, and if it doesn't exist offer to check it out.
if qmk_firmware.exists(): if (cli.args.home / 'Makefile').exists():
cli.log.info('Found qmk_firmware at %s.', str(qmk_firmware)) cli.log.info('Found qmk_firmware at %s.', str(cli.args.home))
else: else:
cli.log.error('qmk_firmware not found!') cli.log.error('Could not find qmk_firmware!')
if question('Would you like to clone %s?' % cli.args.fork): if question(clone_prompt):
git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork)) git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
clone(git_url, cli.args.destination, cli.config.setup.branch) result = git_clone(git_url, cli.args.home, cli.config.setup.branch)
if result != 0:
exit(1)
# Offer to set `user.qmk_home` for them.
if cli.args.home != os.environ['QMK_HOME'] and question(home_prompt):
cli.config['user']['qmk_home'] = str(cli.args.home.absolute())
cli.write_config_option('user', 'qmk_home')
# Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out # Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out
if qmk_firmware.exists(): qmk_bin = cli.args.home / 'bin/qmk'
qmk_bin = qmk_firmware / 'bin' / 'qmk' doctor_cmd = [sys.executable, qmk_bin, 'doctor']
doctor = subprocess.run([sys.executable, str(qmk_bin), 'doctor'])
if doctor.returncode != 0:
cli.log.error('Your build environment is not setup completely.')
if question('Would you like to run util/qmk_install?'): if cli.args.yes:
curdir = os.getcwd() doctor_cmd.append('--yes')
os.chdir(str(qmk_firmware))
process = subprocess.run(['util/qmk_install.sh']) if cli.args.no:
os.chdir(curdir) doctor_cmd.append('--no')
if process.returncode == 0:
cli.log.info('QMK setup complete!') subprocess.run(doctor_cmd)
+1 -1
View File
@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.29 current_version = 0.0.34
commit = True commit = True
tag = True tag = True
tag_name = {new_version} tag_name = {new_version}
+1
View File
@@ -46,6 +46,7 @@ if __name__ == "__main__":
"colorama", "colorama",
"flake8", "flake8",
"hjson", "hjson",
"nose2",
"yapf" "yapf"
], ],
) )