Skip to content

Commit 1d0374f

Browse files
authored
2 parents 41d027a + b373740 commit 1d0374f

File tree

45 files changed

+517
-258
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+517
-258
lines changed

CHANGES

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
2525

2626
### Development
2727

28+
- **Improved typings**
29+
30+
Now [`mypy --strict`] compliant (#859)
31+
32+
[`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict
33+
2834
- Poetry 1.5.1 -> 1.6.1 (#885)
2935

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

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

38-
For security updates, a 1.30.x branch can be maintained for a limited time,
44+
For security updates, a 1.30.x branch can be maintained for a limited time,
3945
if necessary.
4046

4147
## tmuxp 1.30.0 (2023-09-04)
@@ -49,6 +55,7 @@ _Maintenance only, no bug fixes or new features_
4955
This includes fixes made by hand alongside ruff's automated fixes. The more
5056
stringent rules include import sorting, and still runs almost instantly
5157
against the whole codebase.
58+
5259
- CI: `black . --check` now runs on pushes and pull requests
5360

5461
### Packaging

docs/_ext/aafig.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
from sphinx.errors import SphinxError
2222
from sphinx.util.osutil import ensuredir, relative_uri
2323

24+
if t.TYPE_CHECKING:
25+
from sphinx.application import Sphinx
26+
27+
2428
try:
2529
import aafigure
2630
except ImportError:
@@ -32,14 +36,18 @@
3236
DEFAULT_FORMATS = {"html": "svg", "latex": "pdf", "text": None}
3337

3438

35-
def merge_dict(dst, src):
39+
def merge_dict(
40+
dst: t.Dict[str, t.Optional[str]], src: t.Dict[str, t.Optional[str]]
41+
) -> t.Dict[str, t.Optional[str]]:
3642
for k, v in src.items():
3743
if k not in dst:
3844
dst[k] = v
3945
return dst
4046

4147

42-
def get_basename(text, options, prefix="aafig"):
48+
def get_basename(
49+
text: str, options: t.Dict[str, str], prefix: t.Optional[str] = "aafig"
50+
) -> str:
4351
options = options.copy()
4452
if "format" in options:
4553
del options["format"]
@@ -52,7 +60,7 @@ class AafigError(SphinxError):
5260
category = "aafig error"
5361

5462

55-
class AafigDirective(images.Image):
63+
class AafigDirective(images.Image): # type:ignore
5664
"""
5765
Directive to insert an ASCII art figure to be rendered by aafigure.
5866
"""
@@ -71,7 +79,7 @@ class AafigDirective(images.Image):
7179
option_spec = images.Image.option_spec.copy()
7280
option_spec.update(own_option_spec)
7381

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

95103

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

146154

147-
def render_aafigure(app, text, options):
155+
def render_aafigure(
156+
app: "Sphinx", text: str, options: t.Dict[str, str]
157+
) -> t.Tuple[str, str, t.Optional[str], t.Optional[str]]:
148158
"""
149159
Render an ASCII art figure into the requested format output file.
150160
"""
@@ -186,7 +196,7 @@ def render_aafigure(app, text, options):
186196
finally:
187197
if f is not None:
188198
f.close()
189-
return relfn, outfn, id, extra
199+
return relfn, outfn, None, extra
190200
except AafigError:
191201
pass
192202

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

207-
return relfn, outfn, id, extra
217+
return relfn, outfn, None, extra
208218

209219

210-
def setup(app):
220+
def setup(app: "Sphinx") -> None:
211221
app.add_directive("aafig", AafigDirective)
212222
app.connect("doctree-read", render_aafig_images)
213223
app.add_config_value("aafig_format", DEFAULT_FORMATS, "html")

docs/conf.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
if t.TYPE_CHECKING:
1212
from sphinx.application import Sphinx
1313

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

179178

180-
def linkcode_resolve(domain, info):
179+
def linkcode_resolve(domain: str, info: t.Dict[str, str]) -> t.Union[None, str]:
181180
"""
182181
Determine the URL corresponding to Python object
183182
@@ -210,7 +209,8 @@ def linkcode_resolve(domain, info):
210209
except AttributeError:
211210
pass
212211
else:
213-
obj = unwrap(obj)
212+
if callable(obj):
213+
obj = unwrap(obj)
214214

215215
try:
216216
fn = inspect.getsourcefile(obj)

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,12 @@ exclude_lines = [
138138
]
139139

140140
[tool.mypy]
141+
strict = true
141142
files = [
142143
"src/",
143144
"tests/",
144145
]
146+
enable_incomplete_feature = ["Unpack"]
145147

146148
[[tool.mypy.overrides]]
147149
module = [

src/tmuxp/_compat.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
else:
1313
import pdb
1414

15-
breakpoint = pdb.set_trace # type: ignore
15+
breakpoint = pdb.set_trace
1616

1717

1818
console_encoding = sys.__stdout__.encoding

src/tmuxp/_types.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Internal, :const:`typing.TYPE_CHECKING` guarded :term:`type annotations <annotation>`
2+
3+
These are _not_ to be imported at runtime as `typing_extensions` is not
4+
bundled with tmuxp. Usage example:
5+
6+
>>> import typing as t
7+
8+
>>> if t.TYPE_CHECKING:
9+
... from tmuxp._types import PluginConfigSchema
10+
...
11+
"""
12+
import typing as t
13+
14+
from typing_extensions import NotRequired, TypedDict
15+
16+
17+
class PluginConfigSchema(TypedDict):
18+
plugin_name: NotRequired[str]
19+
tmux_min_version: NotRequired[str]
20+
tmux_max_version: NotRequired[str]
21+
tmux_version_incompatible: NotRequired[t.List[str]]
22+
libtmux_min_version: NotRequired[str]
23+
libtmux_max_version: NotRequired[str]
24+
libtmux_version_incompatible: NotRequired[t.List[str]]
25+
tmuxp_min_version: NotRequired[str]
26+
tmuxp_max_version: NotRequired[str]
27+
tmuxp_version_incompatible: NotRequired[t.List[str]]

src/tmuxp/cli/debug_info.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ def command_debug_info(
2929
Print debug info to submit with Issues.
3030
"""
3131

32-
def prepend_tab(strings):
32+
def prepend_tab(strings: t.List[str]) -> t.List[str]:
3333
"""
3434
Prepend tab to strings in list.
3535
"""
3636
return ["\t%s" % x for x in strings]
3737

38-
def output_break():
38+
def output_break() -> str:
3939
"""
4040
Generate output break.
4141
"""
4242
return "-" * 25
4343

44-
def format_tmux_resp(std_resp):
44+
def format_tmux_resp(std_resp: tmux_cmd) -> str:
4545
"""
4646
Format tmux command response for tmuxp stdout.
4747
"""

src/tmuxp/cli/edit.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def create_edit_subparser(
2222
def command_edit(
2323
workspace_file: t.Union[str, pathlib.Path],
2424
parser: t.Optional[argparse.ArgumentParser] = None,
25-
):
25+
) -> None:
2626
workspace_file = find_workspace_file(workspace_file)
2727

2828
sys_editor = os.environ.get("EDITOR", "vim")

src/tmuxp/cli/freeze.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ class CLIFreezeNamespace(argparse.Namespace):
3131
force: t.Optional[bool]
3232

3333

34-
def session_completion(ctx, params, incomplete):
35-
server = Server()
36-
choices = [session.name for session in server.sessions]
37-
return sorted(str(c) for c in choices if str(c).startswith(incomplete))
38-
39-
4034
def create_freeze_subparser(
4135
parser: argparse.ArgumentParser,
4236
) -> argparse.ArgumentParser:
@@ -177,12 +171,14 @@ def extract_workspace_format(
177171

178172
workspace_format = extract_workspace_format(dest)
179173
if not is_valid_ext(workspace_format):
180-
workspace_format = prompt_choices(
174+
_workspace_format = prompt_choices(
181175
"Couldn't ascertain one of [%s] from file name. Convert to"
182176
% ", ".join(valid_workspace_formats),
183-
choices=valid_workspace_formats,
177+
choices=t.cast(t.List[str], valid_workspace_formats),
184178
default="yaml",
185179
)
180+
assert is_valid_ext(_workspace_format)
181+
workspace_format = _workspace_format
186182

187183
if workspace_format == "yaml":
188184
workspace = configparser.dump(

src/tmuxp/cli/import_config.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def command_import(
5959
workspace_file: str,
6060
print_list: str,
6161
parser: argparse.ArgumentParser,
62-
):
62+
) -> None:
6363
"""Import a teamocil/tmuxinator config."""
6464

6565

@@ -116,9 +116,14 @@ def create_import_subparser(
116116
return parser
117117

118118

119+
class ImportConfigFn(t.Protocol):
120+
def __call__(self, workspace_dict: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
121+
...
122+
123+
119124
def import_config(
120125
workspace_file: str,
121-
importfunc: t.Callable,
126+
importfunc: ImportConfigFn,
122127
parser: t.Optional[argparse.ArgumentParser] = None,
123128
) -> None:
124129
existing_workspace_file = ConfigReader._from_file(pathlib.Path(workspace_file))

src/tmuxp/cli/load.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def load_plugins(session_config: t.Dict[str, t.Any]) -> t.List[t.Any]:
153153
return plugins
154154

155155

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

src/tmuxp/cli/utils.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ def tmuxp_echo(
3434

3535
def prompt(
3636
name: str,
37-
default: t.Any = None,
37+
default: t.Optional[str] = None,
3838
value_proc: t.Optional[t.Callable[[str], str]] = None,
39-
) -> t.Any:
39+
) -> str:
4040
"""Return user input from command line.
4141
:meth:`~prompt`, :meth:`~prompt_bool` and :meth:`prompt_choices` are from
4242
`flask-script`_. See the `flask-script license`_.
@@ -107,15 +107,12 @@ def prompt_yes_no(name: str, default: bool = True) -> bool:
107107
return prompt_bool(name, default=default)
108108

109109

110-
_C = t.TypeVar("_C")
111-
112-
113110
def prompt_choices(
114111
name: str,
115-
choices: t.Union[t.List[_C], t.Tuple[str, _C]],
116-
default: t.Optional[_C] = None,
112+
choices: t.Union[t.List[str], t.Tuple[str, str]],
113+
default: t.Optional[str] = None,
117114
no_choice: t.Sequence[str] = ("none",),
118-
) -> t.Optional[_C]:
115+
) -> t.Optional[str]:
119116
"""Return user input from command line from set of provided choices.
120117
:param name: prompt text
121118
:param choices: list or tuple of available choices. Choices may be
@@ -125,8 +122,8 @@ def prompt_choices(
125122
:rtype: str
126123
"""
127124

128-
_choices = []
129-
options = []
125+
_choices: t.List[str] = []
126+
options: t.List[str] = []
130127

131128
for choice in choices:
132129
if isinstance(choice, str):

src/tmuxp/config_reader.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ def _load(format: "FormatLiteral", content: str) -> t.Dict[str, t.Any]:
3636
{'session_name': 'my session'}
3737
"""
3838
if format == "yaml":
39-
return yaml.load(
40-
content,
41-
Loader=yaml.SafeLoader,
39+
return t.cast(
40+
t.Dict[str, t.Any],
41+
yaml.load(
42+
content,
43+
Loader=yaml.SafeLoader,
44+
),
4245
)
4346
elif format == "json":
44-
return json.loads(content)
47+
return t.cast(t.Dict[str, t.Any], json.loads(content))
4548
else:
4649
raise NotImplementedError(f"{format} not supported in configuration")
4750

src/tmuxp/exc.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ class TmuxpPluginException(TmuxpException):
8080

8181

8282
class BeforeLoadScriptNotExists(OSError):
83-
def __init__(self, *args, **kwargs) -> None:
83+
def __init__(self, *args: object, **kwargs: object) -> None:
8484
super().__init__(*args, **kwargs)
8585

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

8888

8989
@implements_to_string
@@ -106,5 +106,5 @@ def __init__(
106106
f"{self.output}"
107107
)
108108

109-
def __str__(self):
109+
def __str__(self) -> str:
110110
return self.message

src/tmuxp/log.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ def template(
115115

116116
return levelname + asctime + name
117117

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

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

127127
date_format = "%H:%m:%S"
128-
formatting = self.converter(record.created) # type:ignore
128+
formatting = self.converter(record.created)
129129
record.asctime = time.strftime(date_format, formatting)
130130

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

0 commit comments

Comments
 (0)