diff --git a/.travis.yml b/.travis.yml index 3e1c8869d8f..3d575c1d778 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,13 @@ matrix: env: TOXENV=requirements - python: "3.5" env: TOXENV=lint + - python: "3.5" + env: TOXENV=typing - python: "3.5" env: TOXENV=py35 + allow_failures: + - python: "3.5" + env: TOXENV=typing cache: directories: - $HOME/.cache/pip diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d92c434d22b..fb1594d5b3f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,6 +8,8 @@ import subprocess import sys import threading +from typing import Optional, List + from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, @@ -16,7 +18,7 @@ from homeassistant.const import ( ) -def validate_python(): +def validate_python() -> None: """Validate we're running the right Python version.""" major, minor = sys.version_info[:2] req_major, req_minor = REQUIRED_PYTHON_VER @@ -27,7 +29,7 @@ def validate_python(): sys.exit(1) -def ensure_config_path(config_dir): +def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" import homeassistant.config as config_util lib_dir = os.path.join(config_dir, 'deps') @@ -56,7 +58,7 @@ def ensure_config_path(config_dir): sys.exit(1) -def ensure_config_file(config_dir): +def ensure_config_file(config_dir: str) -> str: """Ensure configuration file exists.""" import homeassistant.config as config_util config_path = config_util.ensure_config_exists(config_dir) @@ -68,7 +70,7 @@ def ensure_config_file(config_dir): return config_path -def get_arguments(): +def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" import homeassistant.config as config_util parser = argparse.ArgumentParser( @@ -125,12 +127,12 @@ def get_arguments(): arguments = parser.parse_args() if os.name != "posix" or arguments.debug or arguments.runner: - arguments.daemon = False + setattr(arguments, 'daemon', False) return arguments -def daemonize(): +def daemonize() -> None: """Move current process to daemon process.""" # Create first fork pid = os.fork() @@ -155,7 +157,7 @@ def daemonize(): os.dup2(outfd.fileno(), sys.stderr.fileno()) -def check_pid(pid_file): +def check_pid(pid_file: str) -> None: """Check that HA is not already running.""" # Check pid file try: @@ -177,7 +179,7 @@ def check_pid(pid_file): sys.exit(1) -def write_pid(pid_file): +def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: @@ -187,7 +189,7 @@ def write_pid(pid_file): sys.exit(1) -def closefds_osx(min_fd, max_fd): +def closefds_osx(min_fd: int, max_fd: int) -> None: """Make sure file descriptors get closed when we restart. We cannot call close on guarded fds, and we cannot easily test which fds @@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd): pass -def cmdline(): +def cmdline() -> List[str]: """Collect path and arguments to re-execute the current hass instance.""" if sys.argv[0].endswith('/__main__.py'): modulepath = os.path.dirname(sys.argv[0]) @@ -213,16 +215,17 @@ def cmdline(): return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon'] -def setup_and_run_hass(config_dir, args): +def setup_and_run_hass(config_dir: str, + args: argparse.Namespace) -> Optional[int]: """Setup HASS and run.""" from homeassistant import bootstrap # Run a simple daemon runner process on Windows to handle restarts if os.name == 'nt' and '--runner' not in sys.argv: - args = cmdline() + ['--runner'] + nt_args = cmdline() + ['--runner'] while True: try: - subprocess.check_call(args) + subprocess.check_call(nt_args) sys.exit(0) except subprocess.CalledProcessError as exc: if exc.returncode != RESTART_EXIT_CODE: @@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args): log_rotate_days=args.log_rotate_days) if hass is None: - return + return None if args.open_ui: def open_browser(event): @@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args): return exit_code -def try_to_restart(): +def try_to_restart() -> None: """Attempt to clean up state and start a new homeassistant instance.""" # Things should be mostly shut down already at this point, now just try # to clean up things that may have been left behind. @@ -303,7 +306,7 @@ def try_to_restart(): os.execv(args[0], args) -def main(): +def main() -> int: """Start Home Assistant.""" validate_python() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8b3d3ee6f23..71e6b88e310 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -6,9 +6,11 @@ import os import sys from collections import defaultdict from threading import RLock +from typing import Any, Optional, Dict import voluptuous as vol + import homeassistant.components as core_components from homeassistant.components import group, persistent_notification import homeassistant.config as conf_util @@ -202,9 +204,14 @@ def prepare_setup_platform(hass, config, domain, platform_name): # pylint: disable=too-many-branches, too-many-statements, too-many-arguments -def from_config_dict(config, hass=None, config_dir=None, enable_log=True, - verbose=False, skip_pip=False, - log_rotate_days=None): +def from_config_dict(config: Dict[str, Any], + hass: Optional[core.HomeAssistant]=None, + config_dir: Optional[str]=None, + enable_log: bool=True, + verbose: bool=False, + skip_pip: bool=False, + log_rotate_days: Any=None) \ + -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a config dict. Dynamically loads required components and its dependencies. @@ -266,8 +273,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, return hass -def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, - log_rotate_days=None): +def from_config_file(config_path: str, + hass: Optional[core.HomeAssistant]=None, + verbose: bool=False, + skip_pip: bool=True, + log_rotate_days: Any=None): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter if given, diff --git a/homeassistant/config.py b/homeassistant/config.py index bf0cbdba23b..2450453863f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -73,14 +73,14 @@ CORE_CONFIG_SCHEMA = vol.Schema({ }) -def get_default_config_dir(): +def get_default_config_dir() -> str: """Put together the default configuration directory based on OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ else os.path.expanduser('~') return os.path.join(data_dir, CONFIG_DIR_NAME) -def ensure_config_exists(config_dir, detect_location=True): +def ensure_config_exists(config_dir: str, detect_location: bool=True) -> str: """Ensure a config file exists in given configuration directory. Creating a default one if needed. diff --git a/homeassistant/core.py b/homeassistant/core.py index 82ec20c82f9..aec9cce5612 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -57,7 +57,7 @@ class CoreState(enum.Enum): running = "RUNNING" stopping = "STOPPING" - def __str__(self): + def __str__(self) -> str: """Return the event.""" return self.value @@ -75,11 +75,11 @@ class HomeAssistant(object): self.state = CoreState.not_running @property - def is_running(self): + def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state == CoreState.running - def start(self): + def start(self) -> None: """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) @@ -90,7 +90,7 @@ class HomeAssistant(object): self.pool.block_till_done() self.state = CoreState.running - def block_till_stopped(self): + def block_till_stopped(self) -> int: """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() request_restart = threading.Event() @@ -132,7 +132,7 @@ class HomeAssistant(object): return RESTART_EXIT_CODE if request_restart.isSet() else 0 - def stop(self): + def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") self.state = CoreState.stopping @@ -290,7 +290,7 @@ class EventBus(object): # available to execute this listener it might occur that the # listener gets lined up twice to be executed. # This will make sure the second time it does nothing. - onetime_listener.run = True + setattr(onetime_listener, 'run', True) self.remove_listener(event_type, onetime_listener) diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index a0e16efc7d1..87771045b66 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -3,7 +3,7 @@ import importlib import os -def run(args): +def run(args: str) -> int: """Run a script.""" scripts = [fil[:-3] for fil in os.listdir(os.path.dirname(__file__)) if fil.endswith('.py') and fil != '__init__.py'] @@ -19,4 +19,4 @@ def run(args): return 1 script = importlib.import_module('homeassistant.scripts.' + args[0]) - return script.run(args[1:]) + return script.run(args[1:]) # type: ignore diff --git a/requirements_all.txt b/requirements_all.txt index e6f1c1530bf..50c8e4bfca9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ pytz>=2016.6.1 pip>=7.0.0 jinja2>=2.8 voluptuous==0.8.9 +typing>=3,<4 sqlalchemy==1.0.14 # homeassistant.components.isy994 diff --git a/requirements_test.txt b/requirements_test.txt index 649859f2506..a1e799b7cc5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,3 +7,4 @@ pytest-timeout>=1.0.0 pytest-capturelog>=0.7 pydocstyle>=1.0.0 requests_mock>=1.0 +mypy-lang>=0.4 diff --git a/setup.py b/setup.py index 32d24b56003..3d2d4b4e2c2 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ REQUIRES = [ 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.8.9', + 'typing>=3,<4', 'sqlalchemy==1.0.14', ] diff --git a/tox.ini b/tox.ini index bc5acbd71cc..a7fdf3bcb49 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, lint, requirements +envlist = py34, py35, lint, requirements, typing skip_missing_interpreters = True [testenv] @@ -29,3 +29,8 @@ basepython = python3 deps = commands = python script/gen_requirements_all.py validate + +[testenv:typing] +basepython = python3 +commands = + mypy --silent-imports homeassistant