Skip to content

Commit

Permalink
Adopt Ruff and use stricter MyPy settings
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner committed Jul 28, 2024
1 parent dc9a501 commit b49ad58
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 75 deletions.
4 changes: 0 additions & 4 deletions .flake8

This file was deleted.

4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
env: [flake8, mypy]
env:
- ruff
- mypy

steps:
- uses: actions/checkout@v3
Expand Down
53 changes: 53 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
target-version = "py39" # Pin Ruff to Python 3.9
output-format = "full"
line-length = 95

[lint]
preview = true
select = [
# "ANN", # flake8-annotations
"C4", # flake8-comprehensions
"COM", # flake8-commas
"B", # flake8-bugbear
"DTZ", # flake8-datetimez
"E", # pycodestyle
"EM", # flake8-errmsg
"EXE", # flake8-executable
"F", # pyflakes
"FA", # flake8-future-annotations
"FLY", # flynt
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"INT", # flake8-gettext
"LOG", # flake8-logging
"PERF", # perflint
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"SIM", # flake8-simplify
"SLOT", # flake8-slots
"TCH", # flake8-type-checking
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
]
ignore = [
"E116",
"E241",
"E251",
]

[lint.per-file-ignores]
"tests/*" = [
"ANN", # tests don't need annotations
]

[lint.isort]
forced-separate = [
"tests",
]
required-imports = [
"from __future__ import annotations",
]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ clean-mypyfiles:

.PHONY: style-check
style-check:
@flake8
@ruff check

.PHONY: type-check
type-check:
Expand Down
36 changes: 33 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ test = [
"defusedxml>=0.7.1", # for secure XML/HTML parsing
]
lint = [
"flake8",
"ruff==0.5.5",
"mypy",
"docutils-stubs",
"types-docutils",
]
standalone = [
"Sphinx>=5",
Expand All @@ -73,4 +73,34 @@ include = [
]

[tool.mypy]
ignore_missing_imports = true
python_version = "3.9"
packages = [
"sphinxcontrib",
"tests",
]
exclude = [
"tests/roots",
]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
explicit_package_bases = true
extra_checks = true
no_implicit_reexport = true
show_column_numbers = true
show_error_context = true
strict_optional = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = [
"type-arg",
"redundant-self",
"truthy-iterable",
"ignore-without-code",
"unused-awaitable",
]
74 changes: 35 additions & 39 deletions sphinxcontrib/qthelp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
import re
from collections.abc import Iterable
from os import path
from typing import Any, cast
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast

from docutils import nodes
from docutils.nodes import Node
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.locale import get_translation
Expand All @@ -22,6 +21,9 @@
from sphinx.util.osutil import canon_path, make_filename
from sphinx.util.template import SphinxRenderer

if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx

__version__ = '1.0.8'
__version_info__ = (1, 0, 8)
Expand Down Expand Up @@ -78,7 +80,7 @@ def init(self) -> None:
self.link_suffix = '.html'
# self.config.html_style = 'traditional.css'

def get_theme_config(self) -> tuple[str, dict]:
def get_theme_config(self) -> tuple[str, dict[str, str | int | bool]]:
return self.config.qthelp_theme, self.config.qthelp_theme_options

def handle_finish(self) -> None:
Expand All @@ -97,55 +99,55 @@ def build_qhp(self, outdir: str | os.PathLike[str], outname: str) -> None:

sections = []
matcher = NodeMatcher(addnodes.compact_paragraph, toctree=True)
for node in tocdoc.traverse(matcher): # type: addnodes.compact_paragraph
for node in tocdoc.findall(matcher):
sections.extend(self.write_toc(node))

for indexname, indexcls, content, collapse in self.domain_indices:
for indexname, indexcls, _content, _collapse in self.domain_indices:
item = section_template % {'title': indexcls.localname,
'ref': indexname + self.out_suffix}
sections.append(' ' * 4 * 4 + item)
sections = '\n'.join(sections) # type: ignore
sections = '\n'.join(sections) # type: ignore[assignment]

# keywords
keywords = []
index = IndexEntries(self.env).create_index(self, group_entries=False)
for (key, group) in index:
for title, (refs, subitems, key_) in group:
for (_group_key, group) in index:
for title, (refs, subitems, _category_key) in group:
keywords.extend(self.build_keywords(title, refs, subitems))
keywords = '\n'.join(keywords) # type: ignore
keywords = '\n'.join(keywords) # type: ignore[assignment]

# it seems that the "namespace" may not contain non-alphanumeric
# characters, and more than one successive dot, or leading/trailing
# dots, are also forbidden
if self.config.qthelp_namespace:
nspace = self.config.qthelp_namespace
else:
nspace = 'org.sphinx.%s.%s' % (outname, self.config.version)
nspace = f'org.sphinx.{outname}.{self.config.version}'

nspace = re.sub(r'[^a-zA-Z0-9.\-]', '', nspace)
nspace = re.sub(r'\.+', '.', nspace).strip('.')
nspace = nspace.lower()

# write the project file
with open(path.join(outdir, outname + '.qhp'), 'w', encoding='utf-8') as f:
body = render_file('project.qhp', outname=outname,
title=self.config.html_title, version=self.config.version,
project=self.config.project, namespace=nspace,
master_doc=self.config.master_doc,
sections=sections, keywords=keywords,
files=self.get_project_files(outdir))
f.write(body)
body = render_file('project.qhp', outname=outname,
title=self.config.html_title, version=self.config.version,
project=self.config.project, namespace=nspace,
master_doc=self.config.master_doc,
sections=sections, keywords=keywords,
files=self.get_project_files(outdir))
filename = Path(outdir, f'{outname}.qhp')
filename.write_text(body, encoding='utf-8')

homepage = 'qthelp://' + posixpath.join(
nspace, 'doc', self.get_target_uri(self.config.master_doc))
startpage = 'qthelp://' + posixpath.join(nspace, 'doc', 'index%s' % self.link_suffix)
startpage = 'qthelp://' + posixpath.join(nspace, 'doc', f'index{self.link_suffix}')

logger.info(__('writing collection project file...'))
with open(path.join(outdir, outname + '.qhcp'), 'w', encoding='utf-8') as f:
body = render_file('project.qhcp', outname=outname,
title=self.config.html_short_title,
homepage=homepage, startpage=startpage)
f.write(body)
body = render_file('project.qhcp', outname=outname,
title=self.config.html_short_title,
homepage=homepage, startpage=startpage)
filename = Path(outdir, f'{outname}.qhcp')
filename.write_text(body, encoding='utf-8')

def isdocnode(self, node: Node) -> bool:
if not isinstance(node, nodes.list_item):
Expand All @@ -156,9 +158,7 @@ def isdocnode(self, node: Node) -> bool:
return False
if not isinstance(node[0][0], nodes.reference):
return False
if not isinstance(node[1], nodes.bullet_list):
return False
return True
return isinstance(node[1], nodes.bullet_list)

def write_toc(self, node: Node, indentlevel: int = 4) -> list[str]:
parts: list[str] = []
Expand All @@ -167,8 +167,7 @@ def write_toc(self, node: Node, indentlevel: int = 4) -> list[str]:
reference = cast(nodes.reference, compact_paragraph[0])
link = reference['refuri']
title = html.escape(reference.astext()).replace('"', '"')
item = '<section title="%(title)s" ref="%(ref)s">' % \
{'title': title, 'ref': link}
item = f'<section title="{title}" ref="{link}">'
parts.append(' ' * 4 * indentlevel + item)

bullet_list = cast(nodes.bullet_list, node[1])
Expand All @@ -185,10 +184,7 @@ def write_toc(self, node: Node, indentlevel: int = 4) -> list[str]:
item = section_template % {'title': title, 'ref': link}
item = ' ' * 4 * indentlevel + item
parts.append(item.encode('ascii', 'xmlcharrefreplace').decode())
elif isinstance(node, nodes.bullet_list):
for subnode in node:
parts.extend(self.write_toc(subnode, indentlevel))
elif isinstance(node, addnodes.compact_paragraph):
elif isinstance(node, (nodes.bullet_list, addnodes.compact_paragraph)):
for subnode in node:
parts.extend(self.write_toc(subnode, indentlevel))

Expand All @@ -203,16 +199,16 @@ def keyword_item(self, name: str, ref: Any) -> str:
# descr = groupdict.get('descr')
if shortname.endswith('()'):
shortname = shortname[:-2]
id = html.escape('%s.%s' % (id, shortname), True)
id = html.escape(f'{id}.{shortname}', True)
else:
id = None

nameattr = html.escape(name, quote=True)
refattr = html.escape(ref[1], quote=True)
if id:
item = ' ' * 12 + '<keyword name="%s" id="%s" ref="%s"/>' % (nameattr, id, refattr)
item = ' ' * 12 + f'<keyword name="{nameattr}" id="{id}" ref="{refattr}"/>'
else:
item = ' ' * 12 + '<keyword name="%s" ref="%s"/>' % (nameattr, refattr)
item = ' ' * 12 + f'<keyword name="{nameattr}" ref="{refattr}"/>'
item.encode('ascii', 'xmlcharrefreplace')
return item

Expand All @@ -224,7 +220,7 @@ def build_keywords(self, title: str, refs: list[Any], subitems: Any) -> list[str
if len(refs) == 1:
keywords.append(self.keyword_item(title, refs[0]))
elif len(refs) > 1:
for i, ref in enumerate(refs): # XXX
for _i, ref in enumerate(refs): # XXX # NoQA: FURB148
# item = (' '*12 +
# '<keyword name="%s [%d]" ref="%s"/>' % (
# title, i, ref))
Expand All @@ -242,7 +238,7 @@ def get_project_files(self, outdir: str | os.PathLike[str]) -> list[str]:
project_files = []
staticdir = path.join(outdir, '_static')
imagesdir = path.join(outdir, self.imagedir)
for root, dirs, files in os.walk(outdir):
for root, _dirs, files in os.walk(outdir):
resourcedir = root.startswith((staticdir, imagesdir))
for fn in sorted(files):
if (resourcedir and not fn.endswith('.js')) or fn.endswith('.html'):
Expand Down
Empty file added sphinxcontrib/qthelp/py.typed
Empty file.
15 changes: 6 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from __future__ import annotations

from pathlib import Path

import pytest

import sphinx

pytest_plugins = 'sphinx.testing.fixtures'
pytest_plugins = (
'sphinx.testing.fixtures',
)


@pytest.fixture(scope='session')
def rootdir():
if sphinx.version_info[:2] < (7, 2):
from sphinx.testing.path import path

return path(__file__).parent.abspath() / 'roots'

def rootdir() -> Path:
return Path(__file__).resolve().parent / 'roots'
2 changes: 2 additions & 0 deletions tests/roots/test-basic/conf.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from __future__ import annotations

project = 'Python'
2 changes: 2 additions & 0 deletions tests/roots/test-need-escaped/conf.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from __future__ import annotations

project = 'need <b>"escaped"</b> project'
smartquotes = False
Loading

0 comments on commit b49ad58

Please sign in to comment.