-
Notifications
You must be signed in to change notification settings - Fork 861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix all mypy typechecking errors, add a lot of type annotations #1399
Changes from all commits
b79d59d
e743883
5db9b6f
905b46f
de64ef2
c9121d7
70676a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,7 +85,7 @@ class Markdown: | |
callable which accepts an [`Element`][xml.etree.ElementTree.Element] and returns a `str`. | ||
""" | ||
|
||
def __init__(self, **kwargs): | ||
def __init__(self, **kwargs: Any): | ||
""" | ||
Creates a new Markdown instance. | ||
|
||
|
@@ -183,7 +183,7 @@ def registerExtensions( | |
'Successfully loaded extension "%s.%s".' | ||
% (ext.__class__.__module__, ext.__class__.__name__) | ||
) | ||
elif ext is not None: | ||
elif ext is not None: # type: ignore[unreachable] | ||
raise TypeError( | ||
'Extension "{}.{}" must be of type: "{}.{}"'.format( | ||
ext.__class__.__module__, ext.__class__.__name__, | ||
|
@@ -417,11 +417,11 @@ def convertFile( | |
# Read the source | ||
if input: | ||
if isinstance(input, str): | ||
input_file = codecs.open(input, mode="r", encoding=encoding) | ||
with codecs.open(input, mode="r", encoding=encoding) as input_file: | ||
text = input_file.read() | ||
else: | ||
input_file = codecs.getreader(encoding)(input) | ||
text = input_file.read() | ||
input_file.close() | ||
with codecs.getreader(encoding)(input) as input_file: | ||
text = input_file.read() | ||
else: | ||
text = sys.stdin.read() | ||
|
||
|
@@ -440,13 +440,13 @@ def convertFile( | |
output_file.close() | ||
else: | ||
writer = codecs.getwriter(encoding) | ||
output_file = writer(output, errors="xmlcharrefreplace") | ||
output_file.write(html) | ||
output_writer = writer(output, errors="xmlcharrefreplace") | ||
output_writer.write(html) | ||
Comment on lines
-443
to
+444
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why this change was made at all. Note that the Contributing Guide states:
I realize that in this instance the variable name is not exposed outside of this method so there is no concern over a backward incompatible change, but the general principle remains. Please, let's refrain from making unnecessary changes just for personal preference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 100% of code body changes in this pull request are in response to error messages produced by mypy. Don't need to tell me this regarding unrelated changes because I'm always the first one to say this as well. This particular change is because mypy doesn't like that these two differently-typed values share the same variable name. With lines 425-429 reverted: $ mypy markdown
markdown/core.py:427: error: Incompatible types in assignment (expression has type "StreamReader", variable has type "StreamReaderWriter") [assignment]
Found 1 error in 1 file (checked 33 source files) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I embrace Python not being strongly typed. If that means making changes like this, then no thank you. |
||
# Don't close here. User may want to write more. | ||
else: | ||
# Encode manually and write bytes to stdout. | ||
html = html.encode(encoding, "xmlcharrefreplace") | ||
sys.stdout.buffer.write(html) | ||
html_bytes = html.encode(encoding, "xmlcharrefreplace") | ||
sys.stdout.buffer.write(html_bytes) | ||
|
||
return self | ||
|
||
|
@@ -482,7 +482,13 @@ def markdown(text: str, **kwargs: Any) -> str: | |
return md.convert(text) | ||
|
||
|
||
def markdownFromFile(**kwargs: Any): | ||
def markdownFromFile( | ||
*, | ||
input: str | BinaryIO | None = None, | ||
output: str | BinaryIO | None = None, | ||
encoding: str | None = None, | ||
**kwargs: Any | ||
) -> None: | ||
Comment on lines
-485
to
+491
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't recall why the specific keyword parameters were removed some years ago, but why did you feel the need to add them back in? I'm not opposed to it if there is a good reason related to this scope of this PR, but I would like to here the reason. However, I am more concerned about why you added position arguments (*)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function's signature doesn't change, I'm just formalizing the de-facto signature. The |
||
""" | ||
Read Markdown text from a file and write output to a file or a stream. | ||
|
||
|
@@ -491,13 +497,11 @@ def markdownFromFile(**kwargs: Any): | |
[`convert`][markdown.Markdown.convert]. | ||
|
||
Keyword arguments: | ||
input (str | BinaryIO): A file name or readable object. | ||
output (str | BinaryIO): A file name or writable object. | ||
encoding (str): Encoding of input and output. | ||
input: A file name or readable object. | ||
output: A file name or writable object. | ||
encoding: Encoding of input and output. | ||
**kwargs: Any arguments accepted by the `Markdown` class. | ||
|
||
""" | ||
md = Markdown(**kwargs) | ||
md.convertFile(kwargs.get('input', None), | ||
kwargs.get('output', None), | ||
kwargs.get('encoding', None)) | ||
md.convertFile(input, output, encoding) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,13 +33,14 @@ | |
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
from markdown import Markdown | ||
from markdown import blockparser | ||
|
||
|
||
class AdmonitionExtension(Extension): | ||
""" Admonition extension for Python-Markdown. """ | ||
|
||
def extendMarkdown(self, md): | ||
def extendMarkdown(self, md: Markdown) -> None: | ||
""" Add Admonition to Markdown instance. """ | ||
md.registerExtension(self) | ||
|
||
|
@@ -59,7 +60,7 @@ def __init__(self, parser: blockparser.BlockParser): | |
super().__init__(parser) | ||
|
||
self.current_sibling: etree.Element | None = None | ||
self.content_indention = 0 | ||
self.content_indent = 0 | ||
|
||
def parse_content(self, parent: etree.Element, block: str) -> tuple[etree.Element | None, str, str]: | ||
"""Get sibling admonition. | ||
|
@@ -74,11 +75,11 @@ def parse_content(self, parent: etree.Element, block: str) -> tuple[etree.Elemen | |
|
||
# We already acquired the block via test | ||
if self.current_sibling is not None: | ||
sibling = self.current_sibling | ||
prev_sibling = self.current_sibling | ||
block, the_rest = self.detab(block, self.content_indent) | ||
self.current_sibling = None | ||
self.content_indent = 0 | ||
return sibling, block, the_rest | ||
return prev_sibling, block, the_rest | ||
Comment on lines
-77
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, we are making unnecessary out-of-scope changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was made in response to mypy errors. Again this is because mypy doesn't like that a variable name is reused with different types in different places. Which is really a limitation of mypy, most other type checkers would deal with this perfectly fine. With lines 78-82 reverted: $ mypy markdown
markdown/extensions/admonition.py:84: error: Incompatible types in assignment (expression has type "Element | None", variable has type "Element") [assignment]
markdown/extensions/admonition.py:87: error: Incompatible types in assignment (expression has type "None", variable has type "Element") [assignment]
markdown/extensions/admonition.py:101: error: Incompatible types in assignment (expression has type "Element | None", variable has type "Element") [assignment]
markdown/extensions/admonition.py:113: error: Incompatible types in assignment (expression has type "None", variable has type "Element") [assignment] |
||
|
||
sibling = self.lastChild(parent) | ||
|
||
|
@@ -147,6 +148,7 @@ def run(self, parent: etree.Element, blocks: list[str]) -> None: | |
p.text = title | ||
p.set('class', self.CLASSNAME_TITLE) | ||
else: | ||
assert sibling is not None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this raise an error based on Markdown input? If so, this is unacceptable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code already has a problem where TBD: It is worth checking whether this bug case can actually happen (in current code already). In any case we should be happy that mypy flagged this. With line 151 reverted: $ mypy markdown
markdown/extensions/admonition.py:152: error: Item "None" of "Element | None" has no attribute "tag" [union-attr]
markdown/extensions/admonition.py:152: error: Item "None" of "Element | None" has no attribute "text" [union-attr]
markdown/extensions/admonition.py:153: error: Item "None" of "Element | None" has no attribute "text" [union-attr]
markdown/extensions/admonition.py:154: error: Item "None" of "Element | None" has no attribute "text" [union-attr]
markdown/extensions/admonition.py:155: error: Argument 1 to "SubElement" has incompatible type "Element | None"; expected "Element" [arg-type]
markdown/extensions/admonition.py:158: error: Incompatible types in assignment (expression has type "Element | None", variable has type "Element") [assignment] |
||
# Sibling is a list item, but we need to wrap it's content should be wrapped in <p> | ||
if sibling.tag in ('li', 'dd') and sibling.text: | ||
text = sibling.text | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ | |
from typing import TYPE_CHECKING, Callable, Any | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
from markdown import Markdown | ||
import xml.etree.ElementTree as etree | ||
|
||
try: # pragma: no cover | ||
|
@@ -150,7 +151,7 @@ def hilite(self, shebang: bool = True) -> str: | |
|
||
if pygments and self.use_pygments: | ||
try: | ||
lexer = get_lexer_by_name(self.lang, **self.options) | ||
lexer = get_lexer_by_name(self.lang or '', **self.options) | ||
except ValueError: | ||
try: | ||
if self.guess_lang: | ||
|
@@ -161,7 +162,7 @@ def hilite(self, shebang: bool = True) -> str: | |
lexer = get_lexer_by_name('text', **self.options) | ||
if not self.lang: | ||
# Use the guessed lexer's language instead | ||
self.lang = lexer.aliases[0] | ||
self.lang = lexer.aliases[0] # type: ignore[attr-defined] | ||
lang_str = f'{self.lang_prefix}{self.lang}' | ||
if isinstance(self.pygments_formatter, str): | ||
try: | ||
|
@@ -254,6 +255,7 @@ class HiliteTreeprocessor(Treeprocessor): | |
""" Highlight source code in code blocks. """ | ||
|
||
config: dict[str, Any] | ||
md: Markdown | ||
|
||
def code_unescape(self, text: str) -> str: | ||
"""Unescape code.""" | ||
|
@@ -270,8 +272,10 @@ def run(self, root: etree.Element) -> None: | |
for block in blocks: | ||
if len(block) == 1 and block[0].tag == 'code': | ||
local_config = self.config.copy() | ||
text = block[0].text | ||
assert text is not None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another assert.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another place where the code currently assumes that something is present there and violently errors if there's no match. mypy doesn't let it slide. TBD: It is worth checking whether this bug case can actually happen (in current code already). In any case we should be happy that mypy flagged this. With lines 275-278 reverted: $ mypy markdown
markdown/extensions/codehilite.py:276: error: Argument 1 to "code_unescape" of "HiliteTreeprocessor" has incompatible type "str | None"; expected "str" [arg-type]
Found 1 error in 1 file (checked 33 source files) |
||
code = CodeHilite( | ||
self.code_unescape(block[0].text), | ||
self.code_unescape(text), | ||
tab_length=self.md.tab_length, | ||
style=local_config.pop('pygments_style', 'default'), | ||
**local_config | ||
|
@@ -288,7 +292,7 @@ def run(self, root: etree.Element) -> None: | |
class CodeHiliteExtension(Extension): | ||
""" Add source code highlighting to markdown code blocks. """ | ||
|
||
def __init__(self, **kwargs): | ||
def __init__(self, **kwargs) -> None: | ||
# define default configs | ||
self.config = { | ||
'linenums': [ | ||
|
@@ -331,7 +335,7 @@ def __init__(self, **kwargs): | |
pass # Assume it's not a boolean value. Use as-is. | ||
self.config[key] = [value, ''] | ||
|
||
def extendMarkdown(self, md): | ||
def extendMarkdown(self, md: Markdown) -> None: | ||
""" Add `HilitePostprocessor` to Markdown instance. """ | ||
hiliter = HiliteTreeprocessor(md) | ||
hiliter.config = self.getConfigs() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you are trying to accomplish here. Does this make it possible for Markdown content to cause an error? If so, that would be unacceptable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With lines 429-431 reverted:
The code before:
where
RE.match(string)
can beNone
, andNone.group()
is an errorThe code after:
The old code doesn't care to check for the
None
case where it would violently error withAttributeError
. mypy doesn't let it slide and exposes a potential bug.The new code directly checks for it and errors with a clearer message.
There is no change regarding which situations an error does or does not happen.
TBD: It is worth checking whether this bug case can actually happen (in current code already). In any case we should be happy that mypy flagged this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, now we know about a bug which we didn't know about before. That is good. But this is not the way to address it. Under no circumstances should source text cause Markdown to raise an error. In fact, that is one of the primary goals of the project as documented on the home page. Therefore, this is not the proper way to fix the bug. The error needs to be silenced and some reasonable text needs to be included in the output (depending on the conditions under which the issue arises).