From 5e4d7e80dce949fe02ff482b5d6bb56bab1db662 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:07:26 +0300 Subject: [PATCH 1/7] Add support for Python 3.13 --- .github/workflows/python.yaml | 6 +++--- setup.py | 1 + tox.ini | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index b2a0222b..143521c5 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.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: - ubuntu-20.04 - 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/setup.py b/setup.py index b408e3a6..2d5ef587 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 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 diff --git a/tox.ini b/tox.ini index 0d85e20a..e1396884 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py36, py37, py38, py39, py310, py311, py312, pypy +envlist = py{38, 39, 310, 311, 312, 313, py} [testenv] commands = make testone From 2a4af20c4737da6363b9d30158cdd9cc96792939 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:07:46 +0300 Subject: [PATCH 2/7] Remove universal wheels, only needed when supporting both Python 2 and 3 --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 setup.cfg 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 From 49c39332fd228966703b7551c1769d0739b6d048 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:02:39 +0300 Subject: [PATCH 3/7] Remove redundant version checks --- setup.py | 2 +- test/test.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 2d5ef587..17ae4128 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,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 diff --git a/test/test.py b/test/test.py index 871cd2e1..17416dc8 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() From b843577857384f9cb58c6027c59b431fe7744286 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:04:23 +0300 Subject: [PATCH 4/7] Drop support for EOL Python 3.8 --- .github/workflows/python.yaml | 2 +- CHANGES.md | 2 +- lib/markdown2.py | 6 +----- setup.py | 3 +-- tox.ini | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 143521c5..ab7e7044 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: - ubuntu-20.04 - macos-latest 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..fec62f77 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -120,17 +120,13 @@ 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 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] diff --git a/setup.py b/setup.py index 17ae4128..14cd4564 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ 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 @@ -57,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/tox.ini b/tox.ini index e1396884..d103b35b 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py{38, 39, 310, 311, 312, 313, py} +envlist = py{39, 310, 311, 312, 313, py} [testenv] commands = make testone From f6ba7543410c75977869cb9c4b3d63c0e1b529ae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:05:57 +0300 Subject: [PATCH 5/7] Upgrade syntax with pyupgrade --py39-plus --- lib/markdown2.py | 130 ++++++++++++++++++------------------ perf/gen_perf_cases.py | 4 +- perf/perf.py | 4 +- perf/strip_cookbook_data.py | 1 - perf/util.py | 2 +- sandbox/wiki.py | 1 - test/markdown.py | 6 +- test/test.py | 4 +- test/test_markdown2.py | 4 +- test/testall.py | 6 +- test/testlib.py | 12 ++-- tools/cutarelease.py | 15 ++--- 12 files changed, 93 insertions(+), 96 deletions(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index fec62f77..4bf841d0 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -123,15 +123,16 @@ 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 # ---- 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 @@ -148,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/ @@ -266,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. @@ -275,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): @@ -335,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) @@ -408,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(): @@ -547,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" @@ -625,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()) @@ -707,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 @@ -1084,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 @@ -1438,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) @@ -1500,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: @@ -1628,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 @@ -1699,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 @@ -1878,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: @@ -1945,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''' @@ -2060,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): @@ -2095,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: @@ -2154,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: @@ -2394,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""" < @@ -2466,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. @@ -2477,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() ] @@ -2571,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 @@ -2752,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) @@ -2917,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. @@ -2930,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) @@ -2957,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) @@ -3074,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:] @@ -3408,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) )) @@ -3421,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) )) @@ -3505,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 ) @@ -3552,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. @@ -3563,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('') @@ -3601,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. @@ -3625,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() @@ -3640,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) @@ -3700,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. @@ -3791,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. @@ -3938,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/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 17416dc8..995db47a 100755 --- a/test/test.py +++ b/test/test.py @@ -39,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': @@ -48,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') From a6d29ef80f30ac4a929d5149598c4d6354e9a267 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:07:52 +0300 Subject: [PATCH 6/7] Fix tox: 'failed with make is not allowed, use allowlist_externals to allow it' --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index d103b35b..815b5bd7 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,5 @@ envlist = py{39, 310, 311, 312, 313, py} [testenv] +allowlist_externals = make commands = make testone From e5c569e6c14550168016d295f3d2caa128dfc01e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:12:45 +0300 Subject: [PATCH 7/7] Use ubuntu-latest = ubuntu-20.04 --- .github/workflows/python.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index ab7e7044..a54cbae8 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -11,7 +11,7 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: - - ubuntu-20.04 + - ubuntu-latest - macos-latest - windows-latest steps: