Move console output utilities to `sphinx.util.display`

- Merge `old_status_iterator` into ``status_iterator``.
  ``old_status_iterator`` was deprecated in version 1.6.
This commit is contained in:
Adam Turner 2022-09-09 06:02:51 +01:00
parent 1b887e4c5c
commit 8c5e7013ea
15 changed files with 240 additions and 183 deletions

View File

@ -22,6 +22,26 @@ The following is a list of deprecated interfaces.
- Removed
- Alternatives
* - ``sphinx.util.status_iterator``
- 6.1
- 8.0
- ``sphinx.util.display.status_iterator``
* - ``sphinx.util.display_chunk``
- 6.1
- 8.0
- ``sphinx.util.display.display_chunk``
* - ``sphinx.util.SkipProgressMessage``
- 6.1
- 8.0
- ``sphinx.util.display.SkipProgressMessage``
* - ``sphinx.util.progress_message``
- 6.1
- 8.0
- ``sphinx.util.display.progress_message``
* - ``sphinx.util.typing.stringify``
- 6.1
- 8.0

View File

@ -35,9 +35,10 @@ from sphinx.project import Project
from sphinx.registry import SphinxComponentRegistry
from sphinx.roles import XRefRole
from sphinx.theming import Theme
from sphinx.util import docutils, logging, progress_message
from sphinx.util import docutils, logging
from sphinx.util.build_phase import BuildPhase
from sphinx.util.console import bold # type: ignore
from sphinx.util.display import progress_message
from sphinx.util.i18n import CatalogRepository
from sphinx.util.logging import prefixed_warnings
from sphinx.util.osutil import abspath, ensuredir, relpath

View File

@ -20,10 +20,10 @@ from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import SphinxError
from sphinx.events import EventManager
from sphinx.locale import __
from sphinx.util import (UnicodeDecodeErrorHandler, get_filetype, import_object, logging,
progress_message, rst, status_iterator)
from sphinx.util import UnicodeDecodeErrorHandler, get_filetype, import_object, logging, rst
from sphinx.util.build_phase import BuildPhase
from sphinx.util.console import bold # type: ignore
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import sphinx_domains
from sphinx.util.i18n import CatalogInfo, CatalogRepository, docname_to_domain
from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath

View File

@ -17,7 +17,8 @@ from docutils.utils import smartquotes
from sphinx import addnodes
from sphinx.builders.html import BuildInfo, StandaloneHTMLBuilder
from sphinx.locale import __
from sphinx.util import logging, status_iterator
from sphinx.util import logging
from sphinx.util.display import status_iterator
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.i18n import format_date
from sphinx.util.osutil import copyfile, ensuredir

View File

@ -19,8 +19,9 @@ from sphinx.builders import Builder
from sphinx.domains.python import pairindextypes
from sphinx.errors import ThemeError
from sphinx.locale import __
from sphinx.util import logging, split_index_msg, status_iterator
from sphinx.util import logging, split_index_msg
from sphinx.util.console import bold # type: ignore
from sphinx.util.display import status_iterator
from sphinx.util.i18n import CatalogInfo, docname_to_domain
from sphinx.util.nodes import extract_messages, traverse_translatable_index
from sphinx.util.osutil import canon_path, ensuredir, relpath

View File

@ -37,7 +37,8 @@ from sphinx.highlighting import PygmentsBridge
from sphinx.locale import _, __
from sphinx.search import js_index
from sphinx.theming import HTMLThemeFactory
from sphinx.util import isurl, logging, md5, progress_message, status_iterator
from sphinx.util import isurl, logging, md5
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import format_date

View File

@ -21,8 +21,9 @@ from sphinx.config import ENUM, Config
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri, SphinxError
from sphinx.locale import _, __
from sphinx.util import logging, progress_message, status_iterator, texescape
from sphinx.util import logging, texescape
from sphinx.util.console import bold, darkgreen # type: ignore
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import SphinxFileOutput, new_document
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.i18n import format_date

View File

@ -14,8 +14,9 @@ from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.locale import __
from sphinx.util import logging, progress_message
from sphinx.util import logging
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.display import progress_message
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import ensuredir, make_filename_from_project
from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter

View File

@ -12,8 +12,9 @@ from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.toctree import TocTree
from sphinx.locale import __
from sphinx.util import logging, progress_message
from sphinx.util import logging
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.display import progress_message
from sphinx.util.nodes import inline_all_toctrees
logger = logging.getLogger(__name__)

View File

@ -19,8 +19,9 @@ from sphinx.config import Config
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri
from sphinx.locale import _, __
from sphinx.util import logging, progress_message, status_iterator
from sphinx.util import logging
from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.nodes import inline_all_toctrees

View File

@ -19,7 +19,8 @@ from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import get_full_modname, logging, status_iterator
from sphinx.util import get_full_modname, logging
from sphinx.util.display import status_iterator
from sphinx.util.nodes import make_refnode
logger = logging.getLogger(__name__)

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import functools
import hashlib
import os
import posixpath
@ -15,14 +14,15 @@ from datetime import datetime
from importlib import import_module
from os import path
from time import mktime, strptime
from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterable, TypeVar
from typing import IO, TYPE_CHECKING, Any, Iterable
from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit
from sphinx.deprecation import RemovedInSphinx70Warning
from sphinx.deprecation import RemovedInSphinx70Warning, deprecated_alias
from sphinx.errors import ExtensionError, FiletypeNotFoundError, SphinxParallelError
from sphinx.locale import __
from sphinx.util import display as _display
from sphinx.util import logging
from sphinx.util.console import bold, colorize, strip_colors, term_width_line # type: ignore
from sphinx.util.console import strip_colors
from sphinx.util.matching import patfilter # noqa: F401
from sphinx.util.nodes import (caption_ref_re, explicit_title_re, # noqa: F401
nested_parse_with_titles, split_explicit_title)
@ -444,90 +444,6 @@ def isurl(url: str) -> bool:
return bool(url) and '://' in url
def display_chunk(chunk: Any) -> str:
if isinstance(chunk, (list, tuple)):
if len(chunk) == 1:
return str(chunk[0])
return f'{chunk[0]} .. {chunk[-1]}'
return str(chunk)
T = TypeVar('T')
def old_status_iterator(iterable: Iterable[T], summary: str, color: str = "darkgreen",
stringify_func: Callable[[Any], str] = display_chunk
) -> Generator[T, None, None]:
l = 0
for item in iterable:
if l == 0:
logger.info(bold(summary), nonl=True)
l = 1
logger.info(stringify_func(item), color=color, nonl=True)
logger.info(" ", nonl=True)
yield item
if l == 1:
logger.info('')
# new version with progress info
def status_iterator(iterable: Iterable[T], summary: str, color: str = "darkgreen",
length: int = 0, verbosity: int = 0,
stringify_func: Callable[[Any], str] = display_chunk
) -> Generator[T, None, None]:
if length == 0:
yield from old_status_iterator(iterable, summary, color, stringify_func)
return
l = 0
summary = bold(summary)
for item in iterable:
l += 1
s = '%s[%3d%%] %s' % (summary, 100 * l / length, colorize(color, stringify_func(item)))
if verbosity:
s += '\n'
else:
s = term_width_line(s)
logger.info(s, nonl=True)
yield item
if l > 0:
logger.info('')
class SkipProgressMessage(Exception):
pass
class progress_message:
def __init__(self, message: str) -> None:
self.message = message
def __enter__(self) -> None:
logger.info(bold(self.message + '... '), nonl=True)
def __exit__(
self, exc_type: type[Exception], exc_value: Exception, traceback: Any
) -> bool:
if isinstance(exc_value, SkipProgressMessage):
logger.info(__('skipped'))
if exc_value.args:
logger.info(*exc_value.args)
return True
elif exc_type:
logger.info(__('failed'))
else:
logger.info(__('done'))
return False
def __call__(self, f: Callable) -> Callable:
@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with self:
return f(*args, **kwargs)
return wrapper
def epoch_to_rfc1123(epoch: float) -> str:
"""Convert datetime format epoch to RFC1123."""
from babel.dates import format_datetime
@ -567,3 +483,19 @@ def xmlname_checker() -> re.Pattern:
start_chars_regex = convert(name_start_chars)
name_chars_regex = convert(name_chars)
return re.compile(f'({start_chars_regex})({start_chars_regex}|{name_chars_regex})*')
deprecated_alias('sphinx.util',
{
'display_chunk': _display.display_chunk,
'status_iterator': _display.status_iterator,
'SkipProgressMessage': _display.SkipProgressMessage,
'progress_message': _display.progress_message,
},
RemovedInSphinx70Warning,
{
'display_chunk': 'sphinx.util.display.display_chunk',
'status_iterator': 'sphinx.util.display.status_iterator',
'SkipProgressMessage': 'sphinx.util.display.SkipProgressMessage',
'progress_message': 'sphinx.util.display.progress_message',
})

87
sphinx/util/display.py Normal file
View File

@ -0,0 +1,87 @@
from __future__ import annotations
import functools
from typing import Any, Callable, Iterable, Iterator, TypeVar
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import bold, colorize, term_width_line # type: ignore
if False:
from types import TracebackType
logger = logging.getLogger(__name__)
def display_chunk(chunk: Any) -> str:
if isinstance(chunk, (list, tuple)):
if len(chunk) == 1:
return str(chunk[0])
return f'{chunk[0]} .. {chunk[-1]}'
return str(chunk)
T = TypeVar('T')
def status_iterator(
iterable: Iterable[T],
summary: str,
color: str = 'darkgreen',
length: int = 0,
verbosity: int = 0,
stringify_func: Callable[[Any], str] = display_chunk,
) -> Iterator[T]:
if length == 0:
logger.info(bold(summary), nonl=True)
for i, item in enumerate(iterable, start=1):
item_str = colorize(color, stringify_func(item))
if length == 0:
logger.info(item_str, nonl=True)
logger.info(' ', nonl=True)
else:
s = f'{bold(summary)}[{int(100 * i / length): >3d}%] {item_str}'
if verbosity:
logger.info(s + '\n', nonl=True)
else:
logger.info(term_width_line(s), nonl=True)
yield item
logger.info('')
class SkipProgressMessage(Exception):
pass
class progress_message:
def __init__(self, message: str) -> None:
self.message = message
def __enter__(self) -> None:
logger.info(bold(self.message + '... '), nonl=True)
def __exit__(
self,
typ: type[BaseException] | None,
val: BaseException | None,
tb: TracebackType | None,
) -> bool:
if isinstance(val, SkipProgressMessage):
logger.info(__('skipped'))
if val.args:
logger.info(*val.args)
return True
elif val:
logger.info(__('failed'))
else:
logger.info(__('done'))
return False
def __call__(self, f: Callable) -> Callable:
@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with self:
return f(*args, **kwargs)
return wrapper

View File

@ -2,15 +2,11 @@
import os
import tempfile
from unittest.mock import patch
import pytest
from sphinx.errors import ExtensionError
from sphinx.testing.util import strip_escseq
from sphinx.util import (SkipProgressMessage, display_chunk, encode_uri, ensuredir,
import_object, logging, parselinenos, progress_message,
status_iterator, xmlname_checker)
from sphinx.util import encode_uri, ensuredir, import_object, parselinenos, xmlname_checker
def test_encode_uri():
@ -42,14 +38,6 @@ def test_ensuredir():
ensuredir(tmp.name)
def test_display_chunk():
assert display_chunk('hello') == 'hello'
assert display_chunk(['hello']) == 'hello'
assert display_chunk(['hello', 'sphinx', 'world']) == 'hello .. world'
assert display_chunk(('hello',)) == 'hello'
assert display_chunk(('hello', 'sphinx', 'world')) == 'hello .. world'
def test_import_object():
module = import_object('sphinx')
assert module.__name__ == 'sphinx'
@ -70,39 +58,6 @@ def test_import_object():
'(needed for my extension)')
@pytest.mark.sphinx('dummy')
@patch('sphinx.util.console._tw', 40) # terminal width = 40
def test_status_iterator(app, status, warning):
logging.setup(app, status, warning)
# test for old_status_iterator
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... '))
output = strip_escseq(status.getvalue())
assert 'testing ... hello sphinx world \n' in output
assert yields == ['hello', 'sphinx', 'world']
# test for status_iterator (verbosity=0)
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=0))
output = strip_escseq(status.getvalue())
assert 'testing ... [ 33%] hello \r' in output
assert 'testing ... [ 66%] sphinx \r' in output
assert 'testing ... [100%] world \r\n' in output
assert yields == ['hello', 'sphinx', 'world']
# test for status_iterator (verbosity=1)
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=1))
output = strip_escseq(status.getvalue())
assert 'testing ... [ 33%] hello\n' in output
assert 'testing ... [ 66%] sphinx\n' in output
assert 'testing ... [100%] world\n\n' in output
assert yields == ['hello', 'sphinx', 'world']
def test_parselinenos():
assert parselinenos('1,2,3', 10) == [0, 1, 2]
assert parselinenos('4, 5, 6', 10) == [3, 4, 5]
@ -122,44 +77,6 @@ def test_parselinenos():
parselinenos('3-1', 10)
def test_progress_message(app, status, warning):
logging.setup(app, status, warning)
logger = logging.getLogger(__name__)
# standard case
with progress_message('testing'):
logger.info('blah ', nonl=True)
output = strip_escseq(status.getvalue())
assert 'testing... blah done\n' in output
# skipping case
with progress_message('testing'):
raise SkipProgressMessage('Reason: %s', 'error')
output = strip_escseq(status.getvalue())
assert 'testing... skipped\nReason: error\n' in output
# error case
try:
with progress_message('testing'):
raise
except Exception:
pass
output = strip_escseq(status.getvalue())
assert 'testing... failed\n' in output
# decorator
@progress_message('testing')
def func():
logger.info('in func ', nonl=True)
func()
output = strip_escseq(status.getvalue())
assert 'testing... in func done\n' in output
def test_xmlname_check():
checker = xmlname_checker()
assert checker.match('id-pub')

View File

@ -0,0 +1,92 @@
"""Tests util functions."""
from unittest.mock import patch
import pytest
from sphinx.testing.util import strip_escseq
from sphinx.util import logging
from sphinx.util.display import (SkipProgressMessage, display_chunk, progress_message,
status_iterator)
def test_display_chunk():
assert display_chunk('hello') == 'hello'
assert display_chunk(['hello']) == 'hello'
assert display_chunk(['hello', 'sphinx', 'world']) == 'hello .. world'
assert display_chunk(('hello',)) == 'hello'
assert display_chunk(('hello', 'sphinx', 'world')) == 'hello .. world'
@pytest.mark.sphinx('dummy')
@patch('sphinx.util.console._tw', 40) # terminal width = 40
def test_status_iterator(app, status, warning):
logging.setup(app, status, warning)
# # test for old_status_iterator
# status.seek(0)
# status.truncate(0)
# yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... '))
# output = strip_escseq(status.getvalue())
# assert 'testing ... hello sphinx world \n' in output
# assert yields == ['hello', 'sphinx', 'world']
# test for status_iterator (verbosity=0)
status.seek(0)
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=0))
output = strip_escseq(status.getvalue())
assert 'testing ... [ 33%] hello \r' in output
assert 'testing ... [ 66%] sphinx \r' in output
assert 'testing ... [100%] world \r\n' in output
assert yields == ['hello', 'sphinx', 'world']
# test for status_iterator (verbosity=1)
status.seek(0)
status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ',
length=3, verbosity=1))
output = strip_escseq(status.getvalue())
assert 'testing ... [ 33%] hello\n' in output
assert 'testing ... [ 66%] sphinx\n' in output
assert 'testing ... [100%] world\n\n' in output
assert yields == ['hello', 'sphinx', 'world']
def test_progress_message(app, status, warning):
logging.setup(app, status, warning)
logger = logging.getLogger(__name__)
# standard case
with progress_message('testing'):
logger.info('blah ', nonl=True)
output = strip_escseq(status.getvalue())
assert 'testing... blah done\n' in output
# skipping case
with progress_message('testing'):
raise SkipProgressMessage('Reason: %s', 'error')
output = strip_escseq(status.getvalue())
assert 'testing... skipped\nReason: error\n' in output
# error case
try:
with progress_message('testing'):
raise
except Exception:
pass
output = strip_escseq(status.getvalue())
assert 'testing... failed\n' in output
# decorator
@progress_message('testing')
def func():
logger.info('in func ', nonl=True)
func()
output = strip_escseq(status.getvalue())
assert 'testing... in func done\n' in output