diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index b2a0222b..a54cbae8 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -9,15 +9,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: - - ubuntu-20.04 + - ubuntu-latest - macos-latest - windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/CHANGES.md b/CHANGES.md index 0b4d9e2f..85565491 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## python-markdown2 2.5.2 (not yet released) -(nothing yet) +- [pull #605] Add support for Python 3.13, drop EOL 3.8 ## python-markdown2 2.5.1 diff --git a/lib/markdown2.py b/lib/markdown2.py index ad7fecd1..4bf841d0 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -120,22 +120,19 @@ from collections import defaultdict, OrderedDict from abc import ABC, abstractmethod import functools +from collections.abc import Iterable from hashlib import sha256 from random import random -from typing import Any, Callable, Collection, Dict, List, Literal, Optional, Tuple, Type, TypedDict, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, TypedDict, Union +from collections.abc import Collection from enum import IntEnum, auto from os import urandom -if sys.version_info[1] < 9: - from typing import Iterable -else: - from collections.abc import Iterable - # ---- type defs _safe_mode = Literal['replace', 'escape'] -_extras_dict = Dict[str, Any] -_extras_param = Union[List[str], _extras_dict] -_link_patterns = Iterable[Tuple[re.Pattern, Union[str, Callable[[re.Match], str]]]] +_extras_dict = dict[str, Any] +_extras_param = Union[list[str], _extras_dict] +_link_patterns = Iterable[tuple[re.Pattern, Union[str, Callable[[re.Match], str]]]] # ---- globals @@ -152,8 +149,8 @@ def _hash_text(s: str) -> str: return 'md5-' + sha256(SECRET_SALT + s.encode("utf-8")).hexdigest()[32:] # Table of hash values for escaped characters: -g_escape_table = dict([(ch, _hash_text(ch)) - for ch in '\\`*_{}[]()>#+-.!']) +g_escape_table = {ch: _hash_text(ch) + for ch in '\\`*_{}[]()>#+-.!'} # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: # http://bumppo.net/projects/amputator/ @@ -270,7 +267,7 @@ def inner(md: 'Markdown', text, *args, **kwargs): return wrapper -class Markdown(object): +class Markdown: # The dict of "extras" to enable in processing -- a mapping of # extra name to argument for the extra. Most extras do not have an # argument, in which case the value is None. @@ -279,17 +276,17 @@ class Markdown(object): # "extras" argument. extras: _extras_dict # dict of `Extra` names and associated class instances, populated during _setup_extras - extra_classes: Dict[str, 'Extra'] + extra_classes: dict[str, 'Extra'] - urls: Dict[str, str] - titles: Dict[str, str] - html_blocks: Dict[str, str] - html_spans: Dict[str, str] + urls: dict[str, str] + titles: dict[str, str] + html_blocks: dict[str, str] + html_spans: dict[str, str] html_removed_text: str = "{(#HTML#)}" # placeholder removed text that does not trigger bold html_removed_text_compat: str = "[HTML_REMOVED]" # for compat with markdown.py safe_mode: Optional[_safe_mode] - _toc: List[Tuple[int, str, str]] + _toc: list[tuple[int, str, str]] # Used to track when we're inside an ordered or unordered list # (see _ProcessListItems() for details): @@ -339,11 +336,11 @@ def __init__( elif not isinstance(self.extras, dict): # inheriting classes may set `self.extras` as List[str]. # we can't allow it through type hints but we can convert it - self.extras = dict([(e, None) for e in self.extras]) # type:ignore + self.extras = {e: None for e in self.extras} # type:ignore if extras: if not isinstance(extras, dict): - extras = dict([(e, None) for e in extras]) + extras = {e: None for e in extras} self.extras.update(extras) assert isinstance(self.extras, dict) @@ -412,7 +409,7 @@ def _setup_extras(self): if not hasattr(self, '_count_from_header_id') or self.extras['header-ids'].get('reset-count', False): self._count_from_header_id = defaultdict(int) if "metadata" in self.extras: - self.metadata: Dict[str, Any] = {} + self.metadata: dict[str, Any] = {} self.extra_classes = {} for name, klass in Extra._registry.items(): @@ -551,7 +548,7 @@ def toc_sort(entry): # Prepend toc html to output if self.cli or (self.extras['toc'] is not None and self.extras['toc'].get('prepend', False)): - text = '{}\n{}'.format(self._toc_html, text) + text = f'{self._toc_html}\n{text}' text += "\n" @@ -629,20 +626,20 @@ def _extract_metadata(self, text: str) -> str: # _meta_data_pattern only has one capturing group, so we can assume # the returned type to be list[str] - match: List[str] = re.findall(self._meta_data_pattern, metadata_content) + match: list[str] = re.findall(self._meta_data_pattern, metadata_content) if not match: return text - def parse_structured_value(value: str) -> Union[List[Any], Dict[str, Any]]: + def parse_structured_value(value: str) -> Union[list[Any], dict[str, Any]]: vs = value.lstrip() vs = value.replace(v[: len(value) - len(vs)], "\n")[1:] # List if vs.startswith("-"): - r: List[Any] = [] + r: list[Any] = [] # the regex used has multiple capturing groups, so # returned type from findall will be List[List[str]] - match: List[str] + match: list[str] for match in re.findall(self._key_val_list_pat, vs): if match[0] and not match[1] and not match[2]: r.append(match[0].strip()) @@ -711,12 +708,12 @@ def _emacs_vars_oneliner_sub(self, match: re.Match) -> str: if match.group(1).strip() == '-*-' and match.group(4).strip() == '-*-': lead_ws = re.findall(r'^\s*', match.group(1))[0] tail_ws = re.findall(r'\s*$', match.group(4))[0] - return '%s%s' % (lead_ws, '-*-', match.group(2).strip(), '-*-', tail_ws) + return '{}{}'.format(lead_ws, '-*-', match.group(2).strip(), '-*-', tail_ws) start, end = match.span() return match.string[start: end] - def _get_emacs_vars(self, text: str) -> Dict[str, str]: + def _get_emacs_vars(self, text: str) -> dict[str, str]: """Return a dictionary of emacs-style local variables. Parsing is done loosely according to this spec (and according to @@ -1088,7 +1085,7 @@ def _strict_tag_block_sub( for chunk in text.splitlines(True): is_markup = re.match( - r'^(\s{0,%s})(?:(?=))?(?)' % ('' if allow_indent else '0', current_tag), chunk + r'^(\s{{0,{}}})(?:(?=))?(?)'.format('' if allow_indent else '0', current_tag), chunk ) block += chunk @@ -1442,7 +1439,7 @@ def _find_balanced(self, text: str, start: int, open_c: str, close_c: str) -> in i += 1 return i - def _extract_url_and_title(self, text: str, start: int) -> Union[Tuple[str, str, int], Tuple[None, None, None]]: + def _extract_url_and_title(self, text: str, start: int) -> Union[tuple[str, str, int], tuple[None, None, None]]: """Extracts the url and (optional) title from the tail of a link""" # text[start] equals the opening parenthesis idx = self._find_non_whitespace(text, start+1) @@ -1504,10 +1501,10 @@ def _safe_href(self): # omitted ['"<>] for XSS reasons less_safe = r'#/\.!#$%&\(\)\+,/:;=\?@\[\]^`\{\}\|~' # dot seperated hostname, optional port number, not followed by protocol seperator - domain = r'(?:[%s]+(?:\.[%s]+)*)(?:(? str: @@ -1632,8 +1629,8 @@ def _do_links(self, text: str) -> str: if self.safe_mode and not safe_link: result_head = '' % (title_str) else: - result_head = '' % (self._protect_url(url), title_str) - result = '%s%s' % (result_head, link_text) + result_head = ''.format(self._protect_url(url), title_str) + result = '{}{}'.format(result_head, link_text) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) # allowed from curr_pos on, from @@ -1703,8 +1700,8 @@ def _do_links(self, text: str) -> str: if self.safe_mode and not self._safe_href.match(url): result_head = '' % (title_str) else: - result_head = '' % (self._protect_url(url), title_str) - result = '%s%s' % (result_head, link_text) + result_head = ''.format(self._protect_url(url), title_str) + result = '{}{}'.format(result_head, link_text) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) # allowed from curr_pos on, from @@ -1882,9 +1879,9 @@ def _list_sub(self, match: re.Match) -> str: result = self._process_list_items(lst) if self.list_level: - return "<%s%s>\n%s\n" % (lst_type, lst_opts, result, lst_type) + return "<{}{}>\n{}\n".format(lst_type, lst_opts, result, lst_type) else: - return "<%s%s>\n%s\n\n" % (lst_type, lst_opts, result, lst_type) + return "<{}{}>\n{}\n\n".format(lst_type, lst_opts, result, lst_type) @mark_stage(Stage.LISTS) def _do_lists(self, text: str) -> str: @@ -1949,11 +1946,11 @@ def _do_lists(self, text: str) -> str: _list_item_re = re.compile(r''' (\n)? # leading line = \1 (^[ \t]*) # leading whitespace = \2 - (?P%s) [ \t]+ # list marker = \3 + (?P{}) [ \t]+ # list marker = \3 ((?:.+?) # list item text = \4 - (\n{1,2})) # eols = \5 - (?= \n* (\Z | \2 (?P%s) [ \t]+)) - ''' % (_marker_any, _marker_any), + (\n{{1,2}})) # eols = \5 + (?= \n* (\Z | \2 (?P{}) [ \t]+)) + '''.format(_marker_any, _marker_any), re.M | re.X | re.S) _task_list_item_re = re.compile(r''' @@ -2064,8 +2061,7 @@ def _wrap_code(self, inner): wraps in tags. """ yield 0, "" - for tup in inner: - yield tup + yield from inner yield 0, "" def _add_newline(self, inner): @@ -2099,7 +2095,7 @@ def _code_block_sub(self, match: re.Match) -> str: codeblock = self._encode_code(codeblock) - return "\n%s\n\n" % ( + return "\n{}\n\n".format( pre_class_str, code_class_str, codeblock) def _html_class_str_from_tag(self, tag: str) -> str: @@ -2158,7 +2154,7 @@ def _do_code_blocks(self, text: str) -> str: def _code_span_sub(self, match: re.Match) -> str: c = match.group(2).strip(" \t") c = self._encode_code(c) - return "%s" % (self._html_class_str_from_tag("code"), c) + return "{}".format(self._html_class_str_from_tag("code"), c) @mark_stage(Stage.CODE_SPANS) def _do_code_spans(self, text: str) -> str: @@ -2398,7 +2394,7 @@ def _encode_backslash_escapes(self, text: str) -> str: _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) def _auto_link_sub(self, match: re.Match) -> str: g1 = match.group(1) - return '%s' % (self._protect_url(g1), g1) + return '{}'.format(self._protect_url(g1), g1) _auto_email_link_re = re.compile(r""" < @@ -2470,7 +2466,7 @@ def _uniform_outdent( text: str, min_outdent: Optional[str] = None, max_outdent: Optional[str] = None - ) -> Tuple[str, str]: + ) -> tuple[str, str]: ''' Removes the smallest common leading indentation from each (non empty) line of `text` and returns said indent along with the outdented text. @@ -2481,7 +2477,7 @@ def _uniform_outdent( ''' # find the leading whitespace for every line - whitespace: List[Union[str, None]] = [ + whitespace: list[Union[str, None]] = [ re.findall(r'^[ \t]*', line)[0] if line else None for line in text.splitlines() ] @@ -2575,15 +2571,15 @@ class MarkdownWithExtras(Markdown): # ---------------------------------------------------------- class Extra(ABC): - _registry: Dict[str, Type['Extra']] = {} - _exec_order: Dict[Stage, Tuple[List[Type['Extra']], List[Type['Extra']]]] = {} + _registry: dict[str, type['Extra']] = {} + _exec_order: dict[Stage, tuple[list[type['Extra']], list[type['Extra']]]] = {} name: str ''' An identifiable name that users can use to invoke the extra in the Markdown class ''' - order: Tuple[Collection[Union[Stage, Type['Extra']]], Collection[Union[Stage, Type['Extra']]]] + order: tuple[Collection[Union[Stage, type['Extra']]], Collection[Union[Stage, type['Extra']]]] ''' Tuple of two iterables containing the stages/extras this extra will run before and after, respectively @@ -2756,11 +2752,11 @@ def sub(self, match: re.Match) -> str: # indent the body before placing inside the aside block admonition = self.md._uniform_indent( - '%s\n%s\n\n%s\n' % (admonition_type, title, body), + '{}\n{}\n\n{}\n'.format(admonition_type, title, body), self.md.tab, False ) # wrap it in an aside - admonition = '' % (admonition_class, admonition) + admonition = ''.format(admonition_class, admonition) # now indent the whole admonition back to where it started return self.md._uniform_indent(admonition, lead_indent, False) @@ -2921,7 +2917,7 @@ def unhash_code(codeblock): # add back the indent to all lines return "\n%s\n" % self.md._uniform_indent(colored, leading_indent, True) - def tags(self, lexer_name: str) -> Tuple[str, str]: + def tags(self, lexer_name: str) -> tuple[str, str]: ''' Returns the tags that the encoded code block will be wrapped in, based upon the lexer name. @@ -2934,10 +2930,10 @@ def tags(self, lexer_name: str) -> Tuple[str, str]: ''' pre_class = self.md._html_class_str_from_tag('pre') if "highlightjs-lang" in self.md.extras and lexer_name: - code_class = ' class="%s language-%s"' % (lexer_name, lexer_name) + code_class = ' class="{} language-{}"'.format(lexer_name, lexer_name) else: code_class = self.md._html_class_str_from_tag('code') - return ('' % (pre_class, code_class), '') + return (''.format(pre_class, code_class), '') def sub(self, match: re.Match) -> str: lexer_name = match.group(2) @@ -2961,7 +2957,7 @@ def sub(self, match: re.Match) -> str: tags = self.tags(lexer_name) - return "\n%s%s%s\n%s%s\n" % (leading_indent, tags[0], codeblock, leading_indent, tags[1]) + return "\n{}{}{}\n{}{}\n".format(leading_indent, tags[0], codeblock, leading_indent, tags[1]) def run(self, text): return self.fenced_code_block_re.sub(self.sub, text) @@ -3078,7 +3074,7 @@ def run(self, text): # To avoid markdown and : .replace('*', self.md._escape_table['*']) .replace('_', self.md._escape_table['_'])) - link = '%s' % (escaped_href, text[start:end]) + link = '{}'.format(escaped_href, text[start:end]) hash = _hash_text(link) link_from_hash[hash] = link text = text[:start] + hash + text[end:] @@ -3412,7 +3408,7 @@ def sub(self, match: re.Match) -> str: hlines = ['' % self.md._html_class_str_from_tag('table'), '' % self.md._html_class_str_from_tag('thead'), ''] cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)))] for col_idx, col in enumerate(cols): - hlines.append(' %s' % ( + hlines.append(' {}'.format( align_from_col_idx.get(col_idx, ''), self.md._run_span_gamut(col) )) @@ -3425,7 +3421,7 @@ def sub(self, match: re.Match) -> str: hlines.append('') cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)))] for col_idx, col in enumerate(cols): - hlines.append(' %s' % ( + hlines.append(' {}'.format( align_from_col_idx.get(col_idx, ''), self.md._run_span_gamut(col) )) @@ -3509,7 +3505,7 @@ def sub(self, match: re.Match) -> str: self.md._escape_table[waves] = _hash_text(waves) return self.md._uniform_indent( - '\n%s%s%s\n' % (open_tag, self.md._escape_table[waves], close_tag), + '\n{}{}{}\n'.format(open_tag, self.md._escape_table[waves], close_tag), lead_indent, include_empty_lines=True ) @@ -3556,7 +3552,7 @@ def format_cell(text): add_hline('' % self.md._html_class_str_from_tag('thead'), 1) add_hline('', 2) for cell in rows[0]: - add_hline("{}".format(format_cell(cell)), 3) + add_hline(f"{format_cell(cell)}", 3) add_hline('', 2) add_hline('', 1) # Only one header row allowed. @@ -3567,7 +3563,7 @@ def format_cell(text): for row in rows: add_hline('', 2) for cell in row: - add_hline('{}'.format(format_cell(cell)), 3) + add_hline(f'{format_cell(cell)}', 3) add_hline('', 2) add_hline('', 1) add_hline('') @@ -3605,7 +3601,7 @@ def test(self, text): # ---- internal support functions -def calculate_toc_html(toc: Union[List[Tuple[int, str, str]], None]) -> Optional[str]: +def calculate_toc_html(toc: Union[list[tuple[int, str, str]], None]) -> Optional[str]: """Return the HTML for the current TOC. This expects the `_toc` attribute to have been set on this instance. @@ -3629,7 +3625,7 @@ def indent(): if not lines[-1].endswith(""): lines[-1] += "" lines.append("%s" % indent()) - lines.append('%s
  • %s' % ( + lines.append('{}
  • {}'.format( indent(), id, name)) while len(h_stack) > 1: h_stack.pop() @@ -3644,7 +3640,7 @@ class UnicodeWithAttrs(str): possibly attach some attributes. E.g. the "toc_html" attribute when the "toc" extra is used. """ - metadata: Optional[Dict[str, str]] = None + metadata: Optional[dict[str, str]] = None toc_html: Optional[str] = None ## {{{ http://code.activestate.com/recipes/577257/ (r1) @@ -3704,7 +3700,7 @@ def _regex_from_encoded_pattern(s: str) -> re.Pattern: # Recipe: dedent (0.1.2) -def _dedentlines(lines: List[str], tabsize: int = 8, skip_first_line: bool = False) -> List[str]: +def _dedentlines(lines: list[str], tabsize: int = 8, skip_first_line: bool = False) -> list[str]: """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines "lines" is a list of lines to dedent. @@ -3795,7 +3791,7 @@ def _dedent(text: str, tabsize: int = 8, skip_first_line: bool = False) -> str: return ''.join(lines) -class _memoized(object): +class _memoized: """Decorator that caches a function's return value each time it is called. If called later with the same arguments, the cached value is returned, and not re-evaluated. @@ -3942,7 +3938,7 @@ def main(argv=None): formatter_class=_NoReflowFormatter ) parser.add_argument('--version', action='version', - version='%(prog)s {version}'.format(version=__version__)) + version=f'%(prog)s {__version__}') parser.add_argument('paths', nargs='*', help=( 'optional list of files to convert.' diff --git a/perf/gen_perf_cases.py b/perf/gen_perf_cases.py index 28b9e18c..8a45f3e4 100755 --- a/perf/gen_perf_cases.py +++ b/perf/gen_perf_cases.py @@ -104,9 +104,9 @@ def _markdown_from_aspn_html(html): title = None escaped_href = href.replace('(', '\\(').replace(')', '\\)') if title is None: - replacement = '[%s](%s)' % (content, escaped_href) + replacement = '[{}]({})'.format(content, escaped_href) else: - replacement = '[%s](%s "%s")' % (content, escaped_href, + replacement = '[{}]({} "{}")'.format(content, escaped_href, title.replace('"', "'")) markdown = markdown[:start] + replacement + markdown[end:] diff --git a/perf/perf.py b/perf/perf.py index ad500d8e..ed8bd864 100755 --- a/perf/perf.py +++ b/perf/perf.py @@ -34,7 +34,7 @@ def time_markdown_py(cases_dir, repeat): for i in range(repeat): start = clock() for path in glob(join(cases_dir, "*.text")): - f = open(path, 'r') + f = open(path) content = f.read() f.close() try: @@ -59,7 +59,7 @@ def time_markdown2_py(cases_dir, repeat): for i in range(repeat): start = clock() for path in glob(join(cases_dir, "*.text")): - f = open(path, 'r') + f = open(path) content = f.read() f.close() markdowner.convert(content) diff --git a/perf/strip_cookbook_data.py b/perf/strip_cookbook_data.py index 47cceb79..d92597b8 100644 --- a/perf/strip_cookbook_data.py +++ b/perf/strip_cookbook_data.py @@ -1,4 +1,3 @@ - from os.path import * from pprint import pformat diff --git a/perf/util.py b/perf/util.py index e32d0f8b..7fcc862f 100644 --- a/perf/util.py +++ b/perf/util.py @@ -30,7 +30,7 @@ def wrapper(*args, **kw): return func(*args, **kw) finally: total_time = clock() - start_time - print("%s took %.3fs" % (func.__name__, total_time)) + print("{} took {:.3f}s".format(func.__name__, total_time)) return wrapper def hotshotit(func): diff --git a/sandbox/wiki.py b/sandbox/wiki.py index f270b636..fbe0894a 100644 --- a/sandbox/wiki.py +++ b/sandbox/wiki.py @@ -1,4 +1,3 @@ - import sys import re from os.path import * diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5e409001..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index b408e3a6..14cd4564 100755 --- a/setup.py +++ b/setup.py @@ -18,11 +18,11 @@ License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 -Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 +Programming Language :: Python :: 3.13 Operating System :: OS Independent Topic :: Software Development :: Libraries :: Python Modules Topic :: Software Development :: Documentation @@ -32,7 +32,7 @@ extras_require = { "code_syntax_highlighting": ["pygments>=2.7.3"], - "wavedrom": ["wavedrom; python_version>='3.7'"], + "wavedrom": ["wavedrom"], "latex": ['latex2mathml; python_version>="3.8.1"'], } # nested listcomp to combine all optional extras into convenient "all" option @@ -56,7 +56,7 @@ ] }, description="A fast and complete Python implementation of Markdown", - python_requires=">=3.8, <4", + python_requires=">=3.9, <4", extras_require=extras_require, classifiers=classifiers.strip().split("\n"), long_description="""markdown2: A fast and complete Python implementation of Markdown. diff --git a/test/markdown.py b/test/markdown.py index e18336b1..b2ef85da 100644 --- a/test/markdown.py +++ b/test/markdown.py @@ -116,7 +116,7 @@ def is_block_level (tag) : (re.compile(">"), ">"), (re.compile("\""), """)] -ENTITY_NORMALIZATION_EXPRESSIONS_SOFT = [ (re.compile("&(?!\#)"), "&"), +ENTITY_NORMALIZATION_EXPRESSIONS_SOFT = [ (re.compile(r"&(?!\#)"), "&"), (re.compile("<"), "<"), (re.compile(">"), ">"), (re.compile("\""), """)] @@ -325,7 +325,7 @@ def toxml(self): value = self.attribute_values[attr] value = self.doc.normalizeEntities(value, avoidDoubleNormalizing=True) - buffer += ' %s="%s"' % (attr, value) + buffer += ' {}="{}"'.format(attr, value) # Now let's actually append the children @@ -672,7 +672,7 @@ def run (self, lines) : LINK_ANGLED_RE = BRK + r'\s*\(<([^\)]*)>\)' # [text]() IMAGE_LINK_RE = r'\!' + BRK + r'\s*\(([^\)]*)\)' # ![alttxt](http://x.com/) REFERENCE_RE = BRK+ r'\s*\[([^\]]*)\]' # [Google][3] -IMAGE_REFERENCE_RE = r'\!' + BRK + '\s*\[([^\]]*)\]' # ![alt text][2] +IMAGE_REFERENCE_RE = r'\!' + BRK + r'\s*\[([^\]]*)\]' # ![alt text][2] NOT_STRONG_RE = r'( \* )' # stand-alone * or _ AUTOLINK_RE = r'<(http://[^>]*)>' # AUTOMAIL_RE = r'<([^> \!]*@[^> ]*)>' # diff --git a/test/test.py b/test/test.py index 871cd2e1..995db47a 100755 --- a/test/test.py +++ b/test/test.py @@ -27,10 +27,7 @@ def setup(): import pygments # noqa except ImportError: pygments_dir = join(top_dir, "deps", "pygments") - if sys.version_info[0] <= 2: - sys.path.insert(0, pygments_dir) - else: - sys.path.insert(0, pygments_dir + "3") + sys.path.insert(0, pygments_dir + "3") if __name__ == "__main__": logging.basicConfig() @@ -42,7 +39,7 @@ def setup(): try: mod = importlib.import_module(extra_lib) except ImportError: - warnings.append("skipping %s tests ('%s' module not found)" % (extra_lib, extra_lib)) + warnings.append("skipping {} tests ('{}' module not found)".format(extra_lib, extra_lib)) default_tags.append("-%s" % extra_lib) else: if extra_lib == 'pygments': @@ -51,7 +48,7 @@ def setup(): tag = "pygments<2.14" else: tag = "pygments>=2.14" - warnings.append("skipping %s tests (pygments %s found)" % (tag, mod.__version__)) + warnings.append("skipping {} tests (pygments {} found)".format(tag, mod.__version__)) default_tags.append("-%s" % tag) retval = testlib.harness(testdir_from_ns=testdir_from_ns, diff --git a/test/test_markdown2.py b/test/test_markdown2.py index d6618052..06313af9 100755 --- a/test/test_markdown2.py +++ b/test/test_markdown2.py @@ -152,7 +152,7 @@ def generate_tests(cls): if exists(opts_path): try: with warnings.catch_warnings(record=True) as caught_warnings: - opts = eval(open(opts_path, 'r').read()) + opts = eval(open(opts_path).read()) for warning in caught_warnings: print("WARNING: loading %s generated warning: %s - lineno %d" % (opts_path, warning.message, warning.lineno), file=sys.stderr) except Exception: @@ -335,7 +335,7 @@ def _markdown_email_link_sub(match): href, text = match.groups() href = _xml_escape_re.sub(_xml_escape_sub, href) text = _xml_escape_re.sub(_xml_escape_sub, text) - return '%s' % (href, text) + return '{}'.format(href, text) def norm_html_from_html(html): """Normalize (somewhat) Markdown'd HTML. diff --git a/test/testall.py b/test/testall.py index 02ff4895..1158f529 100644 --- a/test/testall.py +++ b/test/testall.py @@ -53,14 +53,14 @@ def testall(): # Don't support Python < 3.5 continue ver_str = "%s.%s" % ver - print("-- test with Python %s (%s)" % (ver_str, python)) + print("-- test with Python {} ({})".format(ver_str, python)) assert ' ' not in python env_args = 'MACOSX_DEPLOYMENT_TARGET= ' if sys.platform == 'darwin' else '' proc = subprocess.Popen( # pass "-u" option to force unbuffered output - "%s%s -u test.py -- -knownfailure" % (env_args, python), + "{}{} -u test.py -- -knownfailure".format(env_args, python), shell=True, stderr=subprocess.PIPE ) @@ -77,6 +77,6 @@ def testall(): for python, ver_str, warning in all_warnings: # now re-print all warnings to make sure they are seen - print('-- warning raised by Python %s (%s) -- %s' % (ver_str, python, warning)) + print('-- warning raised by Python {} ({}) -- {}'.format(ver_str, python, warning)) testall() diff --git a/test/testlib.py b/test/testlib.py index c6244dc4..f0e38663 100644 --- a/test/testlib.py +++ b/test/testlib.py @@ -130,8 +130,8 @@ def wrapper(*args, **kw): finally: total_time = time.time() - start_time if total_time > max_time + tolerance: - raise DurationError(('Test was too long (%.2f s)' - % total_time)) + raise DurationError('Test was too long (%.2f s)' + % total_time) return wrapper return _timedtest @@ -140,7 +140,7 @@ def wrapper(*args, **kw): #---- module api -class Test(object): +class Test: def __init__(self, ns, testmod, testcase, testfn_name, testsuite_class=None): self.ns = ns @@ -439,7 +439,7 @@ def list_tests(testdir_from_ns, tags): if testfile.endswith(".pyc"): testfile = testfile[:-1] print("%s:" % t.shortname()) - print(" from: %s#%s.%s" % (testfile, + print(" from: {}#{}.{}".format(testfile, t.testcase.__class__.__name__, t.testfn_name)) wrapped = textwrap.fill(' '.join(t.tags()), WIDTH-10) print(" tags: %s" % _indent(wrapped, 8, True)) @@ -470,7 +470,7 @@ def __init__(self, stream): def getDescription(self, test): if test._testlib_explicit_tags_: - return "%s [%s]" % (test._testlib_shortname_, + return "{} [{}]".format(test._testlib_shortname_, ', '.join(test._testlib_explicit_tags_)) else: return test._testlib_shortname_ @@ -514,7 +514,7 @@ def printErrorList(self, flavour, errors): self.stream.write("%s\n" % err) -class ConsoleTestRunner(object): +class ConsoleTestRunner: """A test runner class that displays results on the console. It prints out the names of tests as they are run, errors as they diff --git a/tools/cutarelease.py b/tools/cutarelease.py index 86e058fa..54201e22 100755 --- a/tools/cutarelease.py +++ b/tools/cutarelease.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright (c) 2009-2012 Trent Mick """cutarelease -- Cut a release of your project. @@ -154,10 +153,10 @@ def cutarelease(project_name, version_files, dry_run=False): % (changes_path, version)) # Tag version and push. - curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split(b'\n') if t) + curr_tags = {t for t in _capture_stdout(["git", "tag", "-l"]).split(b'\n') if t} if not dry_run and version not in curr_tags: log.info("tag the release") - run('git tag -a "%s" -m "version %s"' % (version, version)) + run('git tag -a "{}" -m "version {}"'.format(version, version)) run('git push --tags') # Optionally release. @@ -193,9 +192,9 @@ def cutarelease(project_name, version_files, dry_run=False): if marker not in changes_txt: raise Error("couldn't find `%s' marker in `%s' " "content: can't prep for subsequent dev" % (marker, changes_path)) - next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr) + next_verline = "{} {}{}".format(marker.rsplit(None, 1)[0], next_version, nyr) changes_txt = changes_txt.replace(marker + '\n', - "%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker)) + "{}\n\n(nothing yet)\n\n\n{}\n".format(next_verline, marker)) if not dry_run: f = codecs.open(changes_path, 'w', 'utf-8') f.write(changes_txt) @@ -221,12 +220,12 @@ def cutarelease(project_name, version_files, dry_run=False): ver_content = ver_content.replace(marker, 'var VERSION = "%s";' % next_version) elif ver_file_type == "python": - marker = "__version_info__ = %r" % (version_info,) + marker = "__version_info__ = {!r}".format(version_info) if marker not in ver_content: raise Error("couldn't find `%s' version marker in `%s' " "content: can't prep for subsequent dev" % (marker, ver_file)) ver_content = ver_content.replace(marker, - "__version_info__ = %r" % (next_version_tuple,)) + "__version_info__ = {!r}".format(next_version_tuple)) elif ver_file_type == "version": ver_content = next_version else: @@ -238,7 +237,7 @@ def cutarelease(project_name, version_files, dry_run=False): f.close() if not dry_run: - run('git commit %s %s -m "prep for future dev"' % ( + run('git commit {} {} -m "prep for future dev"'.format( changes_path, ' '.join(version_files))) run('git push') diff --git a/tox.ini b/tox.ini index 0d85e20a..815b5bd7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,8 @@ # and then run "tox" from this directory. [tox] -envlist = py36, py37, py38, py39, py310, py311, py312, pypy +envlist = py{39, 310, 311, 312, 313, py} [testenv] +allowlist_externals = make commands = make testone