Skip to content

Commit

Permalink
mypy: Strict annotations (#796)
Browse files Browse the repository at this point in the history
Improve typing rules and thereby completions dramatically.
  • Loading branch information
tony authored Dec 28, 2022
2 parents b14fcb4 + 86b044f commit 0d744bb
Show file tree
Hide file tree
Showing 26 changed files with 392 additions and 192 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force

<!-- Maintainers, insert changes / features for the next release here -->

### Breaking changes

- Type annotations: Add strict mypy typings (#796)

## tmuxp 1.22.1 (2022-12-27)

_Maintenance only, no bug fixes or features_
Expand Down
13 changes: 11 additions & 2 deletions src/tmuxp/cli/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

from .utils import prompt_yes_no

if t.TYPE_CHECKING:
from typing_extensions import Literal

AllowedFileTypes = Literal["json", "yaml"]


def create_convert_subparser(
parser: argparse.ArgumentParser,
Expand Down Expand Up @@ -50,6 +55,7 @@ def command_convert(

_, ext = os.path.splitext(workspace_file)
ext = ext.lower()
to_filetype: "AllowedFileTypes"
if ext == ".json":
to_filetype = "yaml"
elif ext in [".yaml", ".yml"]:
Expand All @@ -60,8 +66,11 @@ def command_convert(
configparser = ConfigReader.from_file(workspace_file)
newfile = workspace_file.parent / (str(workspace_file.stem) + f".{to_filetype}")

export_kwargs = {"default_flow_style": False} if to_filetype == "yaml" else {}
new_workspace = configparser.dump(format=to_filetype, indent=2, **export_kwargs)
new_workspace = configparser.dump(
format=to_filetype,
indent=2,
**{"default_flow_style": False} if to_filetype == "yaml" else {},
)

if not answer_yes:
if prompt_yes_no(f"Convert to <{workspace_file}> to {to_filetype}?"):
Expand Down
13 changes: 10 additions & 3 deletions src/tmuxp/cli/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def set_layout_hook(session: Session, hook_name: str) -> None:
session.cmd(*cmd)


def load_plugins(sconf: t.Any) -> t.List[t.Any]:
def load_plugins(sconf: t.Dict[str, t.Any]) -> t.List[t.Any]:
"""
Load and return plugins in workspace
"""
Expand Down Expand Up @@ -158,6 +158,7 @@ def _reattach(builder: WorkspaceBuilder):
If not, ``tmux attach-session`` loads the client to the target session.
"""
assert builder.session is not None
for plugin in builder.plugins:
plugin.reattach(builder.session)
proc = builder.session.cmd("display-message", "-p", "'#S'")
Expand All @@ -181,6 +182,7 @@ def _load_attached(builder: WorkspaceBuilder, detached: bool) -> None:
detached : bool
"""
builder.build()
assert builder.session is not None

if "TMUX" in os.environ: # tmuxp ran from inside tmux
# unset TMUX, save it, e.g. '/tmp/tmux-1000/default,30668,0'
Expand Down Expand Up @@ -214,6 +216,8 @@ def _load_detached(builder: WorkspaceBuilder) -> None:
"""
builder.build()

assert builder.session is not None

if has_gte_version("2.6"): # prepare for both cases
set_layout_hook(builder.session, "client-attached")
set_layout_hook(builder.session, "client-session-changed")
Expand All @@ -231,6 +235,7 @@ def _load_append_windows_to_current_session(builder: WorkspaceBuilder) -> None:
"""
current_attached_session = builder.find_current_attached_session()
builder.build(current_attached_session, append=True)
assert builder.session is not None
if has_gte_version("2.6"): # prepare for both cases
set_layout_hook(builder.session, "client-attached")
set_layout_hook(builder.session, "client-session-changed")
Expand All @@ -244,6 +249,7 @@ def _setup_plugins(builder: WorkspaceBuilder) -> Session:
----------
builder: :class:`workspace.builder.WorkspaceBuilder`
"""
assert builder.session is not None
for plugin in builder.plugins:
plugin.before_script(builder.session)

Expand Down Expand Up @@ -458,8 +464,9 @@ def load_workspace(
)

if choice == "k":
builder.session.kill_session()
tmuxp_echo("Session killed.")
if builder.session is not None:
builder.session.kill_session()
tmuxp_echo("Session killed.")
elif choice == "a":
_reattach(builder)
else:
Expand Down
10 changes: 5 additions & 5 deletions src/tmuxp/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ class ConfigReader:
'{\n "session_name": "my session"\n}'
"""

def __init__(self, content: "RawConfigData"):
def __init__(self, content: "RawConfigData") -> None:
self.content = content

@staticmethod
def _load(format: "FormatLiteral", content: str):
def _load(format: "FormatLiteral", content: str) -> t.Dict[str, t.Any]:
"""Load raw config data and directly return it.
>>> ConfigReader._load("json", '{ "session_name": "my session" }')
Expand All @@ -46,7 +46,7 @@ def _load(format: "FormatLiteral", content: str):
raise NotImplementedError(f"{format} not supported in configuration")

@classmethod
def load(cls, format: "FormatLiteral", content: str):
def load(cls, format: "FormatLiteral", content: str) -> "ConfigReader":
"""Load raw config data into a ConfigReader instance (to dump later).
>>> cfg = ConfigReader.load("json", '{ "session_name": "my session" }')
Expand All @@ -69,7 +69,7 @@ def load(cls, format: "FormatLiteral", content: str):
)

@classmethod
def _from_file(cls, path: pathlib.Path):
def _from_file(cls, path: pathlib.Path) -> t.Dict[str, t.Any]:
r"""Load data from file path directly to dictionary.
**YAML file**
Expand Down Expand Up @@ -114,7 +114,7 @@ def _from_file(cls, path: pathlib.Path):
)

@classmethod
def from_file(cls, path: pathlib.Path):
def from_file(cls, path: pathlib.Path) -> "ConfigReader":
r"""Load data from file path
**YAML file**
Expand Down
8 changes: 6 additions & 2 deletions src/tmuxp/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
~~~~~~~~~
"""
import typing as t

from ._compat import implements_to_string


Expand All @@ -28,7 +30,7 @@ class TmuxpPluginException(TmuxpException):


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

self.strerror = "before_script file '%s' doesn't exist." % self.strerror
Expand All @@ -41,7 +43,9 @@ class BeforeLoadScriptError(Exception):
:meth:`tmuxp.util.run_before_script`.
"""

def __init__(self, returncode, cmd, output=None):
def __init__(
self, returncode: int, cmd: str, output: t.Optional[str] = None
) -> None:
self.returncode = returncode
self.cmd = cmd
self.output = output
Expand Down
124 changes: 64 additions & 60 deletions src/tmuxp/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
}


def setup_logger(logger=None, level="INFO"):
def setup_logger(
logger: t.Optional[logging.Logger] = None, level: str = "INFO"
) -> None:
"""
Setup logging for CLI use.
Expand All @@ -49,80 +51,82 @@ def setup_logger(logger=None, level="INFO"):


def set_style(
message, stylized, style_before=None, style_after=None, prefix="", suffix=""
):
message: str,
stylized: bool,
style_before: str = "",
style_after: str = "",
prefix: str = "",
suffix: str = "",
) -> str:
if stylized:
return prefix + style_before + message + style_after + suffix

return prefix + message + suffix


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.
Parameters
----------
:py:class:`logging.LogRecord` :
object. this is passed in from inside the
:py:meth:`logging.Formatter.format` record.
Returns
-------
str
template for logger message
"""

reset = Style.RESET_ALL
levelname = set_style(
"(%(levelname)s)",
stylized,
style_before=(LEVEL_COLORS.get(record.levelname, "") + Style.BRIGHT),
style_after=Style.RESET_ALL,
suffix=" ",
)
asctime = set_style(
"%(asctime)s",
stylized,
style_before=(Fore.BLACK + Style.DIM + Style.BRIGHT),
style_after=(Fore.RESET + Style.RESET_ALL),
prefix="[",
suffix="]",
)
name = set_style(
"%(name)s",
stylized,
style_before=(Fore.WHITE + Style.DIM + Style.BRIGHT),
style_after=(Fore.RESET + Style.RESET_ALL),
prefix=" ",
suffix=" ",
)

if stylized:
return reset + levelname + asctime + name + reset

return levelname + asctime + name


class LogFormatter(logging.Formatter):
template = default_log_template

def __init__(self, color=True, *args, **kwargs):
def template(
self: logging.Formatter,
record: logging.LogRecord,
stylized: bool = False,
**kwargs: t.Any,
) -> str:
"""
Return the prefix for the log message. Template for Formatter.
Parameters
----------
:py:class:`logging.LogRecord` :
object. this is passed in from inside the
:py:meth:`logging.Formatter.format` record.
Returns
-------
str
template for logger message
"""
reset = Style.RESET_ALL
levelname = set_style(
"(%(levelname)s)",
stylized,
style_before=(LEVEL_COLORS.get(record.levelname, "") + Style.BRIGHT),
style_after=Style.RESET_ALL,
suffix=" ",
)
asctime = set_style(
"%(asctime)s",
stylized,
style_before=(Fore.BLACK + Style.DIM + Style.BRIGHT),
style_after=(Fore.RESET + Style.RESET_ALL),
prefix="[",
suffix="]",
)
name = set_style(
"%(name)s",
stylized,
style_before=(Fore.WHITE + Style.DIM + Style.BRIGHT),
style_after=(Fore.RESET + Style.RESET_ALL),
prefix=" ",
suffix=" ",
)

if stylized:
return reset + levelname + asctime + name + reset

return levelname + asctime + name

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

def format(self, record):
def format(self, record: logging.LogRecord) -> str:
try:
record.message = record.getMessage()
except Exception as e:
record.message = f"Bad message ({e!r}): {record.__dict__!r}"

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

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

Expand Down
Loading

0 comments on commit 0d744bb

Please sign in to comment.