-
Notifications
You must be signed in to change notification settings - Fork 863
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
Enable checking against mypy with minimal set of fixes #1402
Changes from all commits
6919885
b71ac4d
dc41e0d
ab34074
944d89b
3c6cebf
7dc2f9d
0dca660
50612bd
75cb5d7
389cceb
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,6 +85,14 @@ class Markdown: | |
callable which accepts an [`Element`][xml.etree.ElementTree.Element] and returns a `str`. | ||
""" | ||
|
||
tab_length: int | ||
ESCAPED_CHARS: list[str] | ||
block_level_elements: list[str] | ||
registeredExtensions: list[Extension] | ||
stripTopLevelTags: bool | ||
references: dict[str, tuple[str, str]] | ||
htmlStash: util.HtmlStash | ||
|
||
Comment on lines
+88
to
+95
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. These are instance attributes, not class attributes. Will you please stop changing the code to fit a tool which I have repeatedly stated does not fit our needs. I am closing this issue as you are clearly either not understanding my direction or intentionally ignoring it. 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 is the correct way to annotate instance attributes according to all tools and documentation |
||
def __init__(self, **kwargs): | ||
""" | ||
Creates a new Markdown instance. | ||
|
@@ -106,23 +114,23 @@ def __init__(self, **kwargs): | |
|
||
""" | ||
|
||
self.tab_length: int = kwargs.get('tab_length', 4) | ||
self.tab_length = kwargs.get('tab_length', 4) | ||
|
||
self.ESCAPED_CHARS: list[str] = [ | ||
self.ESCAPED_CHARS = [ | ||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!' | ||
] | ||
""" List of characters which get the backslash escape treatment. """ | ||
|
||
self.block_level_elements: list[str] = BLOCK_LEVEL_ELEMENTS.copy() | ||
self.block_level_elements = BLOCK_LEVEL_ELEMENTS.copy() | ||
|
||
self.registeredExtensions: list[Extension] = [] | ||
self.registeredExtensions = [] | ||
self.docType = "" # TODO: Maybe delete this. It does not appear to be used anymore. | ||
self.stripTopLevelTags: bool = True | ||
self.stripTopLevelTags = True | ||
|
||
self.build_parser() | ||
|
||
self.references: dict[str, tuple[str, str]] = {} | ||
self.htmlStash: util.HtmlStash = util.HtmlStash() | ||
self.references = {} | ||
self.htmlStash = util.HtmlStash() | ||
self.registerExtensions(extensions=kwargs.get('extensions', []), | ||
configs=kwargs.get('extension_configs', {})) | ||
self.set_output_format(kwargs.get('output_format', 'xhtml')) | ||
|
@@ -446,7 +454,7 @@ def convertFile( | |
else: | ||
# Encode manually and write bytes to stdout. | ||
html = html.encode(encoding, "xmlcharrefreplace") | ||
sys.stdout.buffer.write(html) | ||
sys.stdout.buffer.write(html) # type: ignore | ||
|
||
return self | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -179,6 +179,9 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]: | |
|
||
class SmartyExtension(Extension): | ||
""" Add Smarty to Markdown. """ | ||
|
||
substitutions: dict[str, str] | ||
|
||
def __init__(self, **kwargs): | ||
self.config = { | ||
'smart_quotes': [True, 'Educate quotes'], | ||
|
@@ -189,7 +192,7 @@ def __init__(self, **kwargs): | |
} | ||
""" Default configuration options. """ | ||
super().__init__(**kwargs) | ||
self.substitutions: dict[str, str] = dict(substitutions) | ||
self.substitutions = dict(substitutions) | ||
self.substitutions.update(self.getConfig('substitutions', default={})) | ||
|
||
def _addPatterns( | ||
|
@@ -199,9 +202,8 @@ def _addPatterns( | |
serie: str, | ||
priority: int, | ||
): | ||
for ind, pattern in enumerate(patterns): | ||
pattern += (md,) | ||
pattern = SubstituteTextPattern(*pattern) | ||
for ind, pattern_args in enumerate(patterns): | ||
pattern = SubstituteTextPattern(*pattern_args, md) | ||
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 the dynamic nature of Python is being taken away here. 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 is indeed a limitation of mypy. 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. To be clear. if the code had been written that way originally when it was submitted in a PR, I would have accepted It. But I would be willing to accept the existing code also (as I did). I don't have an objection to either way, but I do have an objection to a tool forcing one way on me and not allowing the other. That is not a tool I am interested in using. |
||
name = 'smarty-%s-%d' % (serie, ind) | ||
self.inlinePatterns.register(pattern, name, priority-ind) | ||
|
||
|
@@ -253,7 +255,7 @@ def educateQuotes(self, md: Markdown) -> None: | |
) | ||
self._addPatterns(md, patterns, 'quotes', 30) | ||
|
||
def extendMarkdown(self, md): | ||
def extendMarkdown(self, md: Markdown): | ||
configs = self.getConfigs() | ||
self.inlinePatterns: Registry[inlinepatterns.InlineProcessor] = Registry() | ||
if configs['smart_ellipses']: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -188,7 +188,7 @@ class EmStrongItem(NamedTuple): | |
# ----------------------------------------------------------------------------- | ||
|
||
|
||
class Pattern: # pragma: no cover | ||
class _BasePattern: | ||
""" | ||
Base class that inline patterns subclass. | ||
|
||
|
@@ -238,31 +238,18 @@ def getCompiledRegExp(self) -> re.Pattern: | |
""" Return a compiled regular expression. """ | ||
return self.compiled_re | ||
|
||
def handleMatch(self, m: re.Match[str]) -> etree.Element | str: | ||
"""Return a ElementTree element from the given match. | ||
|
||
Subclasses should override this method. | ||
|
||
Arguments: | ||
m: A match object containing a match of the pattern. | ||
|
||
Returns: An ElementTree Element object. | ||
|
||
""" | ||
pass # pragma: no cover | ||
|
||
def type(self) -> str: | ||
""" Return class name, to define pattern type """ | ||
return self.__class__.__name__ | ||
|
||
def unescape(self, text: str) -> str: | ||
""" Return unescaped text given text with an inline placeholder. """ | ||
try: | ||
stash = self.md.treeprocessors['inline'].stashed_nodes | ||
stash = self.md.treeprocessors['inline'].stashed_nodes # type: ignore[attr-defined] | ||
except KeyError: # pragma: no cover | ||
return text | ||
|
||
def get_stash(m): | ||
def get_stash(m: re.Match[str]) -> str: | ||
id = m.group(1) | ||
if id in stash: | ||
value = stash.get(id) | ||
|
@@ -274,6 +261,27 @@ def get_stash(m): | |
return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text) | ||
|
||
|
||
class LegacyPattern(_BasePattern): | ||
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 have very serious objections to the changes to this file. We have API changes. Under no circumstances am I interested in making API changes for type checking. If that means it's not possible to move forward with mypy, then we don't move forward with mypy. To be clear, this specific line of code is not any more or less relevant than any other. However, the changes are rather sweeping and it was not possible to easily highlight and comment on all relevant lines. I'm speaking about most of the changes to this file in general. 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. There is no actual change to the API, all usages can remain the same. But this is still indeed the worst change here. I spent a lot of time thinking of this but it's the best I came up with. We could also solicit ideas regarding this. I'll comment further on this soon. 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. Let me explain what the problem here is. This is the general explanation: For the particular situation at hand: And let's just quote directly from mypy's output:
The particular violation happens regardless of mypy, it's an actual run-time problem if users expect it to work like is widely accepted: If you write: if isinstance(foo, Pattern):
foo.handleMatch(single_argument) it must work, because every instance of An instance of More specific code example: >>> import re
>>> from markdown.inlinepatterns import EscapeInlineProcessor, Pattern
>>> foo = Pattern('a')
>>> foo.handleMatch(re.search('a', 'a')) # OK
>>> foo = EscapeInlineProcessor('a')
>>> isinstance(foo, Pattern)
True
>>> foo.handleMatch(re.search('a', 'a'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: EscapeInlineProcessor.handleMatch() missing 1 required positional argument: 'data' The correct hierarchy would be this: graph TD;
BasePattern["BasePattern<br>defines helper methods only"]-->Pattern["Pattern<br>abstract def handleMatch(m):"];
BasePattern-->InlineProcessor["InlineProcessor<br>abstract def handleMatch(m, data):"];
Pattern-->SimpleTextPattern["SimpleTextPattern<br>def handleMatch(m):"]
Pattern-->SimpleTagPattern["SimpleTagPattern<br>def handleMatch(m):"]
InlineProcessor-->EscapeInlineProcessor["EscapeInlineProcessor<br>def handleMatch(m, data):"]
InlineProcessor-->AsteriskProcessor["AsteriskProcessor<br>def handleMatch(m, data):"]
But the current hierarchy is: graph TD;
Pattern["Pattern<br>defines helper methods<br>abstract def handleMatch(m):"];
Pattern-->InlineProcessor["InlineProcessor<br>abstract def handleMatch(m, data):"];
Pattern-->SimpleTextPattern["SimpleTextPattern<br>def handleMatch(m):"]
Pattern-->SimpleTagPattern["SimpleTagPattern<br>def handleMatch(m):"]
InlineProcessor-->EscapeInlineProcessor["EscapeInlineProcessor<br>def handleMatch(m, data):"]
InlineProcessor-->AsteriskProcessor["AsteriskProcessor<br>def handleMatch(m, data):"]
As such, the entire hierarchy of What my change accomplishes is to construct the correct type hierarchy to mypy and to all typechecking users going forward. 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. See a code search of occurrences in the wild: currently every subclass of Of course at the moment that's not this repository's fault (as it doesn't have 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. There's also this issue report (which I found through the above code search because it's directly inspired by this case). Which if accepted would resolve this 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. An equivalent change to the external stubs would be this: |
||
def handleMatch(self, m: re.Match[str]) -> etree.Element | str: | ||
"""Return a ElementTree element from the given match. | ||
|
||
Subclasses should override this method. | ||
|
||
Arguments: | ||
m: A match object containing a match of the pattern. | ||
|
||
Returns: An ElementTree Element object. | ||
|
||
""" | ||
pass # pragma: no cover | ||
|
||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
Pattern = _BasePattern | ||
else: | ||
Pattern = LegacyPattern | ||
|
||
|
||
class InlineProcessor(Pattern): | ||
""" | ||
Base class that inline processors subclass. | ||
|
@@ -505,13 +513,13 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]: | |
def unescape(self, text: str) -> str: | ||
""" Return unescaped text given text with an inline placeholder. """ | ||
try: | ||
stash = self.md.treeprocessors['inline'].stashed_nodes | ||
stash = self.md.treeprocessors['inline'].stashed_nodes # type: ignore[attr-defined] | ||
except KeyError: # pragma: no cover | ||
return text | ||
|
||
def get_stash(m: re.Match[str]) -> str: | ||
id = m.group(1) | ||
value = stash.get(id) | ||
value: etree.Element | None = stash.get(id) | ||
if value is not None: | ||
try: | ||
return self.md.serializer(value) | ||
|
@@ -523,7 +531,7 @@ def get_stash(m: re.Match[str]) -> str: | |
def backslash_unescape(self, text: str) -> str: | ||
""" Return text with backslash escapes undone (backslashes are restored). """ | ||
try: | ||
RE = self.md.treeprocessors['unescape'].RE | ||
RE = self.md.treeprocessors['unescape'].RE # type: ignore[attr-defined] | ||
except KeyError: # pragma: no cover | ||
return text | ||
|
||
|
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.
This change should be completely unnecessary. The code works just fine and any tool which prevents me from writing code that way is unwanted.
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.
The code can actually sometimes return an instance of
int
rather thanbool
. If we have to, I'll follow the usual over-the-top process of proving to you that this is a bug and writing a test.