Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mypy: Use --strict #859

Merged
merged 39 commits into from Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cd8f3c5
ci(mypy): mypy --strict
tony Dec 31, 2022
0d0cae4
ci(mypy): Enable incomplete feature (Unpack)
tony Sep 23, 2023
fe3e169
refactor!: Add typings from monkeytype
tony Dec 31, 2022
b907890
More autogenerated types
tony Sep 23, 2023
27018ce
chore(mypy): Typings for test_plugin_helpers
tony Sep 23, 2023
fa6e520
chore(mypy): Typings for test_builder
tony Sep 23, 2023
7b39602
chore(mypy): Typings for test_load
tony Jan 7, 2023
d4c7df5
chore(mypy): Typings for test_finder
tony Sep 23, 2023
a0598aa
chore(mypy): Typings for test_builder
tony Sep 23, 2023
0523b9a
chore(mypy): Typings for test_builder
tony Sep 23, 2023
1482ca3
chore(mypy): Typings for test_load.py
tony Sep 23, 2023
16645f3
chore(mypy): Add typings for test_cli.py
tony Sep 23, 2023
cc70fb6
chore(mypy): Add typings for test_util
tony Sep 23, 2023
7bed027
chore(mypy): Basic typings for plugin test partials
tony Sep 23, 2023
9d00b77
docs(conf): Typings for mypy
tony Sep 23, 2023
86cf7db
chore(mypy): Add typings for aafig
tony Sep 23, 2023
74a01fc
feat(_types): TYPE_CHECKING-guarded types, add PluginConfigSchema
tony Sep 23, 2023
74145bf
refactor(plugin): Add typings
tony Sep 23, 2023
28d54c9
chore(mypy): Add typing for plugin tests
tony Sep 23, 2023
ba77831
chore(mypy): Typings for plugin tests
tony Sep 23, 2023
b7749b3
refactor(tests[pluginsystem]): Use double splat
tony Sep 23, 2023
1d310ed
chore(mypy): Typings for tests/fixtures/pluginsystem/plugins
tony Sep 23, 2023
7415da4
chore(mypy): Typings for cli/edit
tony Sep 23, 2023
7d6be7e
chore(mypy): Ignore odd type issue in test_plugin_helpers
tony Sep 23, 2023
83d0aa9
chore(mypy): Add typings for cli/import_config
tony Sep 23, 2023
729ae23
chore(cli[freeze]): Remove session_completion
tony Sep 23, 2023
8df5939
chore(mypy): Add typings for cli/load
tony Sep 23, 2023
df84c46
chore(mypy): Typings for cli/debug_info
tony Sep 23, 2023
68b5628
refactor(mypy): Add typings for cli/utils
tony Sep 23, 2023
c77277e
chore(mypy): Add typings for cli/freeze
tony Sep 23, 2023
c965577
chore(mypy): Fix typings for workspace/freezer
tony Sep 23, 2023
1496f1d
refactor(mypy): Typings for tmuxp/shell
tony Sep 23, 2023
954f36f
chore(mypy): Add typings for plugin
tony Sep 23, 2023
083f5f8
chore(mypy): Typings for config_reader
tony Sep 23, 2023
071ec81
chore(mypy): Add typings exc
tony Sep 23, 2023
4b95fd5
chore(mypy): Add typings for workspace/loader
tony Sep 23, 2023
f0e637d
chore(mypy): Remove unused type: ignore from _compat
tony Sep 23, 2023
23fe049
chore(mypy): Add typings for log
tony Sep 23, 2023
b373740
docs(CHANGES): Note mypy strict compliance
tony Sep 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGES
Expand Up @@ -25,6 +25,12 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force

### Development

- **Improved typings**

Now [`mypy --strict`] compliant (#859)

[`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict

- Poetry 1.5.1 -> 1.6.1 (#885)

## tmuxp 1.30.1 (2023-09-09)
Expand All @@ -35,7 +41,7 @@ _Maintenance only, no bug fixes or new features_

- Cut last python 3.7 release (EOL was June 27th, 2023)

For security updates, a 1.30.x branch can be maintained for a limited time,
For security updates, a 1.30.x branch can be maintained for a limited time,
if necessary.

## tmuxp 1.30.0 (2023-09-04)
Expand All @@ -49,6 +55,7 @@ _Maintenance only, no bug fixes or new features_
This includes fixes made by hand alongside ruff's automated fixes. The more
stringent rules include import sorting, and still runs almost instantly
against the whole codebase.

- CI: `black . --check` now runs on pushes and pull requests

### Packaging
Expand Down
28 changes: 19 additions & 9 deletions docs/_ext/aafig.py
Expand Up @@ -21,6 +21,10 @@
from sphinx.errors import SphinxError
from sphinx.util.osutil import ensuredir, relative_uri

if t.TYPE_CHECKING:
from sphinx.application import Sphinx


try:
import aafigure
except ImportError:
Expand All @@ -32,14 +36,18 @@
DEFAULT_FORMATS = {"html": "svg", "latex": "pdf", "text": None}


def merge_dict(dst, src):
def merge_dict(
dst: t.Dict[str, t.Optional[str]], src: t.Dict[str, t.Optional[str]]
) -> t.Dict[str, t.Optional[str]]:
for k, v in src.items():
if k not in dst:
dst[k] = v
return dst


def get_basename(text, options, prefix="aafig"):
def get_basename(
text: str, options: t.Dict[str, str], prefix: t.Optional[str] = "aafig"
) -> str:
options = options.copy()
if "format" in options:
del options["format"]
Expand All @@ -52,7 +60,7 @@ class AafigError(SphinxError):
category = "aafig error"


class AafigDirective(images.Image):
class AafigDirective(images.Image): # type:ignore
"""
Directive to insert an ASCII art figure to be rendered by aafigure.
"""
Expand All @@ -71,7 +79,7 @@ class AafigDirective(images.Image):
option_spec = images.Image.option_spec.copy()
option_spec.update(own_option_spec)

def run(self):
def run(self) -> t.List[nodes.Node]:
aafig_options = {}
own_options_keys = [self.own_option_spec.keys(), "scale"]
for k, v in self.options.items():
Expand All @@ -93,7 +101,7 @@ def run(self):
return [image_node]


def render_aafig_images(app, doctree):
def render_aafig_images(app: "Sphinx", doctree: nodes.Node) -> None:
format_map = app.builder.config.aafig_format
merge_dict(format_map, DEFAULT_FORMATS)
if aafigure is None:
Expand Down Expand Up @@ -144,7 +152,9 @@ def __init__(self, *args: object, **kwargs: object) -> None:
return super().__init__("aafigure module not installed", *args, **kwargs)


def render_aafigure(app, text, options):
def render_aafigure(
app: "Sphinx", text: str, options: t.Dict[str, str]
) -> t.Tuple[str, str, t.Optional[str], t.Optional[str]]:
"""
Render an ASCII art figure into the requested format output file.
"""
Expand Down Expand Up @@ -186,7 +196,7 @@ def render_aafigure(app, text, options):
finally:
if f is not None:
f.close()
return relfn, outfn, id, extra
return relfn, outfn, None, extra
except AafigError:
pass

Expand All @@ -204,10 +214,10 @@ def render_aafigure(app, text, options):
with open(metadata_fname, "w") as f:
f.write(extra)

return relfn, outfn, id, extra
return relfn, outfn, None, extra


def setup(app):
def setup(app: "Sphinx") -> None:
app.add_directive("aafig", AafigDirective)
app.connect("doctree-read", render_aafig_images)
app.add_config_value("aafig_format", DEFAULT_FORMATS, "html")
Expand Down
6 changes: 3 additions & 3 deletions docs/conf.py
Expand Up @@ -11,7 +11,6 @@
if t.TYPE_CHECKING:
from sphinx.application import Sphinx


# Get the project root dir, which is the parent dir of this
cwd = pathlib.Path(__file__).parent
project_root = cwd.parent
Expand Down Expand Up @@ -177,7 +176,7 @@
aafig_default_options = {"scale": 0.75, "aspect": 0.5, "proportional": True}


def linkcode_resolve(domain, info):
def linkcode_resolve(domain: str, info: t.Dict[str, str]) -> t.Union[None, str]:
"""
Determine the URL corresponding to Python object

Expand Down Expand Up @@ -210,7 +209,8 @@ def linkcode_resolve(domain, info):
except AttributeError:
pass
else:
obj = unwrap(obj)
if callable(obj):
obj = unwrap(obj)

try:
fn = inspect.getsourcefile(obj)
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Expand Up @@ -138,10 +138,12 @@ exclude_lines = [
]

[tool.mypy]
strict = true
files = [
"src/",
"tests/",
]
enable_incomplete_feature = ["Unpack"]

[[tool.mypy.overrides]]
module = [
Expand Down
2 changes: 1 addition & 1 deletion src/tmuxp/_compat.py
Expand Up @@ -12,7 +12,7 @@
else:
import pdb

breakpoint = pdb.set_trace # type: ignore
breakpoint = pdb.set_trace


console_encoding = sys.__stdout__.encoding
Expand Down
27 changes: 27 additions & 0 deletions src/tmuxp/_types.py
@@ -0,0 +1,27 @@
"""Internal, :const:`typing.TYPE_CHECKING` guarded :term:`type annotations <annotation>`

These are _not_ to be imported at runtime as `typing_extensions` is not
bundled with tmuxp. Usage example:

>>> import typing as t

>>> if t.TYPE_CHECKING:
... from tmuxp._types import PluginConfigSchema
...
"""
import typing as t

from typing_extensions import NotRequired, TypedDict


class PluginConfigSchema(TypedDict):
plugin_name: NotRequired[str]
tmux_min_version: NotRequired[str]
tmux_max_version: NotRequired[str]
tmux_version_incompatible: NotRequired[t.List[str]]
libtmux_min_version: NotRequired[str]
libtmux_max_version: NotRequired[str]
libtmux_version_incompatible: NotRequired[t.List[str]]
tmuxp_min_version: NotRequired[str]
tmuxp_max_version: NotRequired[str]
tmuxp_version_incompatible: NotRequired[t.List[str]]
6 changes: 3 additions & 3 deletions src/tmuxp/cli/debug_info.py
Expand Up @@ -29,19 +29,19 @@ def command_debug_info(
Print debug info to submit with Issues.
"""

def prepend_tab(strings):
def prepend_tab(strings: t.List[str]) -> t.List[str]:
"""
Prepend tab to strings in list.
"""
return ["\t%s" % x for x in strings]

def output_break():
def output_break() -> str:
"""
Generate output break.
"""
return "-" * 25

def format_tmux_resp(std_resp):
def format_tmux_resp(std_resp: tmux_cmd) -> str:
"""
Format tmux command response for tmuxp stdout.
"""
Expand Down
2 changes: 1 addition & 1 deletion src/tmuxp/cli/edit.py
Expand Up @@ -22,7 +22,7 @@ def create_edit_subparser(
def command_edit(
workspace_file: t.Union[str, pathlib.Path],
parser: t.Optional[argparse.ArgumentParser] = None,
):
) -> None:
workspace_file = find_workspace_file(workspace_file)

sys_editor = os.environ.get("EDITOR", "vim")
Expand Down
12 changes: 4 additions & 8 deletions src/tmuxp/cli/freeze.py
Expand Up @@ -31,12 +31,6 @@ class CLIFreezeNamespace(argparse.Namespace):
force: t.Optional[bool]


def session_completion(ctx, params, incomplete):
server = Server()
choices = [session.name for session in server.sessions]
return sorted(str(c) for c in choices if str(c).startswith(incomplete))


def create_freeze_subparser(
parser: argparse.ArgumentParser,
) -> argparse.ArgumentParser:
Expand Down Expand Up @@ -177,12 +171,14 @@ def extract_workspace_format(

workspace_format = extract_workspace_format(dest)
if not is_valid_ext(workspace_format):
workspace_format = prompt_choices(
_workspace_format = prompt_choices(
"Couldn't ascertain one of [%s] from file name. Convert to"
% ", ".join(valid_workspace_formats),
choices=valid_workspace_formats,
choices=t.cast(t.List[str], valid_workspace_formats),
default="yaml",
)
assert is_valid_ext(_workspace_format)
workspace_format = _workspace_format

if workspace_format == "yaml":
workspace = configparser.dump(
Expand Down
9 changes: 7 additions & 2 deletions src/tmuxp/cli/import_config.py
Expand Up @@ -59,7 +59,7 @@ def command_import(
workspace_file: str,
print_list: str,
parser: argparse.ArgumentParser,
):
) -> None:
"""Import a teamocil/tmuxinator config."""


Expand Down Expand Up @@ -116,9 +116,14 @@ def create_import_subparser(
return parser


class ImportConfigFn(t.Protocol):
def __call__(self, workspace_dict: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
...


def import_config(
workspace_file: str,
importfunc: t.Callable,
importfunc: ImportConfigFn,
parser: t.Optional[argparse.ArgumentParser] = None,
) -> None:
existing_workspace_file = ConfigReader._from_file(pathlib.Path(workspace_file))
Expand Down
2 changes: 1 addition & 1 deletion src/tmuxp/cli/load.py
Expand Up @@ -153,7 +153,7 @@ def load_plugins(session_config: t.Dict[str, t.Any]) -> t.List[t.Any]:
return plugins


def _reattach(builder: WorkspaceBuilder):
def _reattach(builder: WorkspaceBuilder) -> None:
"""
Reattach session (depending on env being inside tmux already or not)

Expand Down
17 changes: 7 additions & 10 deletions src/tmuxp/cli/utils.py
Expand Up @@ -34,9 +34,9 @@ def tmuxp_echo(

def prompt(
name: str,
default: t.Any = None,
default: t.Optional[str] = None,
value_proc: t.Optional[t.Callable[[str], str]] = None,
) -> t.Any:
) -> str:
"""Return user input from command line.
:meth:`~prompt`, :meth:`~prompt_bool` and :meth:`prompt_choices` are from
`flask-script`_. See the `flask-script license`_.
Expand Down Expand Up @@ -107,15 +107,12 @@ def prompt_yes_no(name: str, default: bool = True) -> bool:
return prompt_bool(name, default=default)


_C = t.TypeVar("_C")


def prompt_choices(
name: str,
choices: t.Union[t.List[_C], t.Tuple[str, _C]],
default: t.Optional[_C] = None,
choices: t.Union[t.List[str], t.Tuple[str, str]],
default: t.Optional[str] = None,
no_choice: t.Sequence[str] = ("none",),
) -> t.Optional[_C]:
) -> t.Optional[str]:
"""Return user input from command line from set of provided choices.
:param name: prompt text
:param choices: list or tuple of available choices. Choices may be
Expand All @@ -125,8 +122,8 @@ def prompt_choices(
:rtype: str
"""

_choices = []
options = []
_choices: t.List[str] = []
options: t.List[str] = []

for choice in choices:
if isinstance(choice, str):
Expand Down
11 changes: 7 additions & 4 deletions src/tmuxp/config_reader.py
Expand Up @@ -36,12 +36,15 @@ def _load(format: "FormatLiteral", content: str) -> t.Dict[str, t.Any]:
{'session_name': 'my session'}
"""
if format == "yaml":
return yaml.load(
content,
Loader=yaml.SafeLoader,
return t.cast(
t.Dict[str, t.Any],
yaml.load(
content,
Loader=yaml.SafeLoader,
),
)
elif format == "json":
return json.loads(content)
return t.cast(t.Dict[str, t.Any], json.loads(content))
else:
raise NotImplementedError(f"{format} not supported in configuration")

Expand Down
6 changes: 3 additions & 3 deletions src/tmuxp/exc.py
Expand Up @@ -80,10 +80,10 @@ class TmuxpPluginException(TmuxpException):


class BeforeLoadScriptNotExists(OSError):
def __init__(self, *args, **kwargs) -> None:
def __init__(self, *args: object, **kwargs: object) -> None:
super().__init__(*args, **kwargs)

self.strerror = "before_script file '%s' doesn't exist." % self.strerror
self.strerror = f"before_script file '{self.strerror}' doesn't exist."


@implements_to_string
Expand All @@ -106,5 +106,5 @@ def __init__(
f"{self.output}"
)

def __str__(self):
def __str__(self) -> str:
return self.message
6 changes: 3 additions & 3 deletions src/tmuxp/log.py
Expand Up @@ -115,8 +115,8 @@ def template(

return levelname + asctime + name

def __init__(self, color: bool = True, *args, **kwargs) -> None:
logging.Formatter.__init__(self, *args, **kwargs)
def __init__(self, color: bool = True, **kwargs: t.Any) -> None:
logging.Formatter.__init__(self, **kwargs)

def format(self, record: logging.LogRecord) -> str:
try:
Expand All @@ -125,7 +125,7 @@ def format(self, record: logging.LogRecord) -> str:
record.message = f"Bad message ({e!r}): {record.__dict__!r}"

date_format = "%H:%m:%S"
formatting = self.converter(record.created) # type:ignore
formatting = self.converter(record.created)
record.asctime = time.strftime(date_format, formatting)

prefix = self.template(record) % record.__dict__
Expand Down