diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12766fb7066..4892b5b88d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,9 +90,13 @@ jobs: - name: Install python dependencies run: | poetry install -E "test coverage lint" + - name: Lint with flake8 - run: | - poetry run flake8 + run: poetry run flake8 + + - name: Lint with mypy + run: poetry run mypy . + - name: Test with pytest continue-on-error: ${{ matrix.tmux-version == 'master' }} run: | diff --git a/CHANGES b/CHANGES index c826574e06a..2513ed89c50 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,7 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force - libtmux updated from v0.12 to v0.14 {issue}`790` - Add [doctest](https://docs.python.org/3/library/doctest.html) w/ [pytest + doctest](https://docs.pytest.org/en/7.1.x/how-to/doctest.html), ({issue}`791`). +- Added basic [mypy](http://mypy-lang.org/) type annotations via {issue}`786` ## tmuxp 1.12.1 (2022-08-04) diff --git a/docs/_ext/aafig.py b/docs/_ext/aafig.py index 8d4ec5d99f0..91a37a66b62 100644 --- a/docs/_ext/aafig.py +++ b/docs/_ext/aafig.py @@ -10,19 +10,15 @@ :author: Leandro Lucarella :license: BOLA, see LICENSE for details """ +import logging import posixpath +from hashlib import sha1 as sha from os import path from docutils import nodes from docutils.parsers.rst.directives import flag, images, nonnegative_int from sphinx.errors import SphinxError -from sphinx.util import ensuredir, logging, relative_uri - -try: - from hashlib import sha1 as sha -except ImportError: - from sha import sha - +from sphinx.util.osutil import ensuredir, relative_uri try: import aafigure diff --git a/docs/conf.py b/docs/conf.py index 53d19ed6251..9b686506a83 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,7 @@ # flake8: NOQA E5 import inspect import sys +import typing as t from os.path import dirname, relpath from pathlib import Path @@ -14,7 +15,7 @@ sys.path.insert(0, str(cwd / "_ext")) # package data -about = {} +about: t.Dict[str, str] = {} with open(project_root / "tmuxp" / "__about__.py") as fp: exec(fp.read(), about) @@ -61,8 +62,8 @@ html_static_path = ["_static"] html_favicon = "_static/favicon.ico" html_theme = "furo" -html_theme_path = [] -html_theme_options = { +html_theme_path: t.List[str] = [] +html_theme_options: t.Dict[str, t.Union[str, t.List[t.Dict[str, str]]]] = { "light_logo": "img/tmuxp.svg", "dark_logo": "img/tmuxp.svg", "footer_icons": [ diff --git a/docs/developing.md b/docs/developing.md index 21b20be17d1..f12aacbd24b 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -334,6 +334,106 @@ this will load the `.tmuxp.yaml` in the root of the project. ``` +## Formatting + +The project uses [black] and [isort] (one after the other). Configurations are in `pyproject.toml` +and `setup.cfg`: + +- `make black isort`: Run `black` first, then `isort` to handle import nuances + +## Linting + +[flake8] and [mypy] run via CI in our GitHub Actions. See the configuration in `pyproject.toml` and +`setup.cfg`. + +### flake8 + +[flake8] provides fast, reliable, barebones styling and linting. + +````{tab} Command + +poetry: + +```console +$ poetry run flake8 +``` + +If you setup manually: + +```console +$ flake8 +``` + +```` + +````{tab} make + +```console +$ make flake8 +``` + +```` + +````{tab} Watch + +```console +$ make watch_flake8 +``` + +requires [`entr(1)`]. + +```` + +````{tab} Configuration + +See `[flake8]` in setup.cfg. + +```{literalinclude} ../setup.cfg +:language: ini +:start-at: "[flake8]" +:end-before: "[isort]" + +``` + +```` + +### mypy + +[mypy] is used for static type checking. + +````{tab} Command + +poetry: + +```console +$ poetry run mypy . +``` + +If you setup manually: + +```console +$ mypy . +``` + +```` + +````{tab} make + +```console +$ make mypy +``` + +```` + +````{tab} Watch + +```console +$ make watch_mypy +``` + +requires [`entr(1)`]. +```` + (gh-actions)= ## Continuous integration @@ -350,6 +450,10 @@ the [gh build site]. [py.test usage argument]: https://pytest.org/latest/usage.html [entr]: http://entrproject.org/ [`entr(1)`]: http://entrproject.org/ +[black]: https://github.com/psf/black +[isort]: https://pypi.org/project/isort/ +[flake8]: https://flake8.pycqa.org/ +[mypy]: http://mypy-lang.org/ [github actions]: https://github.com/features/actions [gh build site]: https://github.com/tmux-python/tmuxp/actions?query=workflow%3Atests [.github/workflows/tests.yml]: https://github.com/tmux-python/tmuxp/blob/master/.github/workflows/tests.yml diff --git a/poetry.lock b/poetry.lock index 2c6d84551c4..1dd6f65dfb6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -883,6 +883,22 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "types-colorama" +version = "0.4.15" +description = "Typing stubs for colorama" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-docutils" +version = "0.19.0" +description = "Typing stubs for docutils" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.3.0" @@ -937,7 +953,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "fd413df6e46ac6f537f0ae451546c067e85f232c665b985929e84068897f6dd9" +content-hash = "8247361fb8db31e108152b0c3fda3aed46f8dca5c8dd5a6dda1b87cb9ee5aef2" [metadata.files] aafigure = [ @@ -1301,6 +1317,14 @@ tornado = [ {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, ] typed-ast = [] +types-colorama = [ + {file = "types-colorama-0.4.15.tar.gz", hash = "sha256:fd128b1e32f3fecec5f09df4366d21498ee86ea31fcf8b4e8f1ade6d0bbf9832"}, + {file = "types_colorama-0.4.15-py3-none-any.whl", hash = "sha256:9cdc88dcde9e8ebafb2fdfaf5cee260452f93e5c57eb5d8b2a7f65b836d4e5d0"}, +] +types-docutils = [ + {file = "types-docutils-0.19.0.tar.gz", hash = "sha256:94936b1961aacda61ec6bb0acf1169cd7830b5230b645855c1d4789baf19685e"}, + {file = "types_docutils-0.19.0-py3-none-any.whl", hash = "sha256:198ed1c0ef6c1a79411da9e1745514eda433d37770e24f26b0e13a302904cc97"}, +] typing-extensions = [] urllib3 = [] watchdog = [] diff --git a/pyproject.toml b/pyproject.toml index a2f3d4c16c3..16107ebfc21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,8 @@ isort = "*" ### Lint ### flake8 = "*" mypy = "*" +types-colorama = "^0.4.15" +types-docutils = "^0.19.0" [tool.poetry.extras] docs = [ @@ -107,8 +109,20 @@ docs = [ test = ["pytest", "pytest-rerunfailures", "pytest-mock", "pytest-watcher"] coverage = ["codecov", "coverage", "pytest-cov"] format = ["black", "isort"] -lint = ["flake8", "mypy"] +lint = ["flake8", "mypy", "types-colorama", "types-docutils"] [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[[tool.mypy.overrides]] +module = [ + "kaptan", + "aafigure", + "libtmux.*", + "IPython.*", + "ptpython.*", + "prompt_toolkit.*", + "bpython" +] +ignore_missing_imports = true diff --git a/setup.cfg b/setup.cfg index 006953379fb..8e5eeed7830 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,12 +5,6 @@ max-line-length = 88 # Stuff we ignore thanks to black: https://github.com/ambv/black/issues/429 extend-ignore = E203,W503 -[tool:pytest] -filterwarnings = - ignore:distutils Version classes are deprecated. Use packaging.version instead. -addopts = --reruns=0 --tb=short --no-header --showlocals --doctest-modules -doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE - [isort] profile = black combine_as_imports= true @@ -21,3 +15,9 @@ known_pytest = pytest,py known_first_party = libtmux,tmuxp sections = FUTURE,STDLIB,PYTEST,THIRDPARTY,FIRSTPARTY,LOCALFOLDER line_length = 88 + +[tool:pytest] +filterwarnings = + ignore:distutils Version classes are deprecated. Use packaging.version instead. +addopts = --reruns=0 --tb=short --no-header --showlocals --doctest-modules +doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE diff --git a/tests/fixtures/structures.py b/tests/fixtures/structures.py new file mode 100644 index 00000000000..5af93b21464 --- /dev/null +++ b/tests/fixtures/structures.py @@ -0,0 +1,13 @@ +import dataclasses +import typing as t + + +@dataclasses.dataclass +class TestConfigData: + expand1: t.Any + expand2: t.Any + expand_blank: t.Any + sampleconfig: t.Any + shell_command_before: t.Any + shell_command_before_session: t.Any + trickle: t.Any diff --git a/tests/test_cli.py b/tests/test_cli.py index 2be26d0e238..426e5abd1b1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ import json import os import pathlib +import typing as t from unittest.mock import MagicMock import pytest @@ -416,7 +417,7 @@ def test_regression_00132_session_name_with_dots( ): yaml_config = FIXTURE_PATH / "workspacebuilder" / "regression_00132_dots.yaml" cli_args = [str(yaml_config)] - inputs = [] + inputs: t.List[str] = [] runner = CliRunner() result = runner.invoke( cli.command_load, cli_args, input="".join(inputs), standalone_mode=False diff --git a/tests/test_config.py b/tests/test_config.py index 5695ae6248d..f956abf4141 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ """Test for tmuxp configuration import, inlining, expanding and export.""" import os import pathlib +import types import typing from typing import Union @@ -13,7 +14,7 @@ from .constants import EXAMPLE_PATH if typing.TYPE_CHECKING: - from .fixtures import config as ConfigFixture + from .fixtures.structures import TestConfigData @pytest.fixture @@ -23,9 +24,16 @@ def config_fixture(): pytest setup (conftest.py) patches os.environ["HOME"], delay execution of os.path.expanduser until here. """ - from .fixtures import config as config_fixture - - return config_fixture + from .fixtures import config as test_config_data + from .fixtures.structures import TestConfigData + + return TestConfigData( + **{ + k: v + for k, v in test_config_data.__dict__.items() + if isinstance(v, types.ModuleType) + } + ) def load_yaml(path: Union[str, pathlib.Path]) -> str: @@ -44,7 +52,7 @@ def load_config(path: Union[str, pathlib.Path]) -> str: ) -def test_export_json(tmp_path: pathlib.Path, config_fixture: "ConfigFixture"): +def test_export_json(tmp_path: pathlib.Path, config_fixture: "TestConfigData"): json_config_file = tmp_path / "config.json" configparser = kaptan.Kaptan() @@ -59,7 +67,7 @@ def test_export_json(tmp_path: pathlib.Path, config_fixture: "ConfigFixture"): assert config_fixture.sampleconfig.sampleconfigdict == new_config_data -def test_export_yaml(tmp_path: pathlib.Path, config_fixture: "ConfigFixture"): +def test_export_yaml(tmp_path: pathlib.Path, config_fixture: "TestConfigData"): yaml_config_file = tmp_path / "config.yaml" configparser = kaptan.Kaptan() @@ -103,13 +111,13 @@ def test_scan_config(tmp_path: pathlib.Path): assert len(configs) == files -def test_config_expand1(config_fixture: "ConfigFixture"): +def test_config_expand1(config_fixture: "TestConfigData"): """Expand shell commands from string to list.""" test_config = config.expand(config_fixture.expand1.before_config) assert test_config == config_fixture.expand1.after_config() -def test_config_expand2(config_fixture: "ConfigFixture"): +def test_config_expand2(config_fixture: "TestConfigData"): """Expand shell commands from string to list.""" unexpanded_dict = load_yaml(config_fixture.expand2.unexpanded_yaml()) expanded_dict = load_yaml(config_fixture.expand2.expanded_yaml()) @@ -228,7 +236,7 @@ def test_inheritance_config(): assert config == inheritance_config_after -def test_shell_command_before(config_fixture: "ConfigFixture"): +def test_shell_command_before(config_fixture: "TestConfigData"): """Config inheritance for the nested 'start_command'.""" test_config = config_fixture.shell_command_before.config_unexpanded test_config = config.expand(test_config) @@ -239,7 +247,7 @@ def test_shell_command_before(config_fixture: "ConfigFixture"): assert test_config == config_fixture.shell_command_before.config_after() -def test_in_session_scope(config_fixture: "ConfigFixture"): +def test_in_session_scope(config_fixture: "TestConfigData"): sconfig = load_yaml(config_fixture.shell_command_before_session.before) config.validate_schema(sconfig) @@ -250,7 +258,7 @@ def test_in_session_scope(config_fixture: "ConfigFixture"): ) -def test_trickle_relative_start_directory(config_fixture: "ConfigFixture"): +def test_trickle_relative_start_directory(config_fixture: "TestConfigData"): test_config = config.trickle(config_fixture.trickle.before) assert test_config == config_fixture.trickle.expected @@ -273,7 +281,7 @@ def test_trickle_window_with_no_pane_config(): } -def test_expands_blank_panes(config_fixture: "ConfigFixture"): +def test_expands_blank_panes(config_fixture: "TestConfigData"): """Expand blank config into full form. Handle ``NoneType`` and 'blank':: diff --git a/tmuxp/_compat.py b/tmuxp/_compat.py index a69ba4c793f..805d6c1cc5c 100644 --- a/tmuxp/_compat.py +++ b/tmuxp/_compat.py @@ -12,17 +12,9 @@ else: import pdb - breakpoint = pdb.set_trace + breakpoint = pdb.set_trace # type: ignore console_encoding = sys.__stdout__.encoding implements_to_string = _identity - - -def console_to_str(s): - """From pypa/pip project, pip.backwardwardcompat. License MIT.""" - try: - return s.decode(console_encoding) - except UnicodeDecodeError: - return s.decode("utf_8") diff --git a/tmuxp/cli/freeze.py b/tmuxp/cli/freeze.py index 2b05fc6f57f..80da186d26f 100644 --- a/tmuxp/cli/freeze.py +++ b/tmuxp/cli/freeze.py @@ -5,10 +5,10 @@ import kaptan from libtmux.server import Server +from tmuxp.exc import TmuxpException from .. import config, util from ..workspacebuilder import freeze -from . import exc from .utils import _validate_choices, get_abs_path, get_config_dir @@ -65,8 +65,8 @@ def command_freeze( session = util.get_session(t) if not session: - raise exc.TmuxpException("Session not found.") - except exc.TmuxpException as e: + raise TmuxpException("Session not found.") + except TmuxpException as e: print(e) return diff --git a/tmuxp/cli/shell.py b/tmuxp/cli/shell.py index eb2821e9b74..6d52a734ed0 100644 --- a/tmuxp/cli/shell.py +++ b/tmuxp/cli/shell.py @@ -84,7 +84,7 @@ def command_shell( exec(command) else: if shell == "pdb" or (os.getenv("PYTHONBREAKPOINT") and PY3 and PYMINOR >= 7): - from ._compat import breakpoint as tmuxp_breakpoint + from tmuxp._compat import breakpoint as tmuxp_breakpoint tmuxp_breakpoint() return diff --git a/tmuxp/conftest.py b/tmuxp/conftest.py index 2a7b6aaf492..b08c6bec33a 100644 --- a/tmuxp/conftest.py +++ b/tmuxp/conftest.py @@ -66,14 +66,15 @@ def monkeypatch_plugin_test_packages(monkeypatch): @pytest.fixture(scope="function") -def socket_name(request): +def socket_name(request) -> str: return "tmuxp_test%s" % next(namer) @pytest.fixture(scope="function") -def server(request: SubRequest, monkeypatch: pytest.MonkeyPatch) -> Server: - tmux = Server() - tmux.socket_name = socket_name +def server( + request: SubRequest, monkeypatch: pytest.MonkeyPatch, socket_name: str +) -> Server: + tmux = Server(socket_name=socket_name) def fin() -> None: tmux.kill_server() @@ -140,7 +141,7 @@ def add_doctest_fixtures( doctest_namespace: t.Dict[str, t.Any], ) -> None: if isinstance(request._pyfuncitem, DoctestItem) and which("tmux"): - doctest_namespace["server"]: "Server" = request.getfixturevalue("server") + doctest_namespace["server"] = request.getfixturevalue("server") session: "Session" = request.getfixturevalue("session") doctest_namespace["session"] = session doctest_namespace["window"] = session.attached_window diff --git a/tmuxp/log.py b/tmuxp/log.py index e79edfb2488..4f54a5fa588 100644 --- a/tmuxp/log.py +++ b/tmuxp/log.py @@ -7,6 +7,7 @@ """ import logging import time +import typing as t from colorama import Fore, Style @@ -56,7 +57,12 @@ def set_style( return prefix + message + suffix -def default_log_template(self, record, stylized=False): +def default_log_template( + self: t.Type[logging.Formatter], + record: logging.LogRecord, + stylized: t.Optional[bool] = False, + **kwargs: t.Any, +) -> str: """ Return the prefix for the log message. Template for Formatter. @@ -76,7 +82,7 @@ def default_log_template(self, record, stylized=False): levelname = set_style( "(%(levelname)s)", stylized, - style_before=(LEVEL_COLORS.get(record.levelname) + Style.BRIGHT), + style_before=(LEVEL_COLORS.get(record.levelname, "") + Style.BRIGHT), style_after=Style.RESET_ALL, suffix=" ", ) @@ -125,7 +131,13 @@ def format(self, record): return formatted.replace("\n", "\n" + parts[0] + " ") -def debug_log_template(self, record): +def debug_log_template( + self: t.Type[logging.Formatter], + record: logging.LogRecord, + stylized: t.Optional[bool] = False, + **kwargs: t.Any, +) -> str: + """ Return the prefix for the log message. Template for Formatter. @@ -143,7 +155,7 @@ def debug_log_template(self, record): reset = Style.RESET_ALL levelname = ( - LEVEL_COLORS.get(record.levelname) + LEVEL_COLORS.get(record.levelname, "") + Style.BRIGHT + "(%(levelname)1.1s)" + Style.RESET_ALL diff --git a/tmuxp/util.py b/tmuxp/util.py index 118345a6110..61c20669a76 100644 --- a/tmuxp/util.py +++ b/tmuxp/util.py @@ -10,10 +10,10 @@ import subprocess import sys +from libtmux._compat import console_to_str from libtmux.exc import LibTmuxException from . import exc -from ._compat import console_to_str logger = logging.getLogger(__name__)