Skip to content
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

Update functional test expected output #5349

Merged
merged 12 commits into from Nov 24, 2021
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions ChangeLog
Expand Up @@ -44,6 +44,15 @@ Release date: TBA

* Fix exception when pyreverse parses ``property function`` of a class.

* The functional ``testutils`` now accept ``end_lineno`` and ``end_column``. Expected
output files without these will trigger a ``DeprecationWarning``. Expected output files
can be easily updated with the ``python tests/test_functional.py --update-functional-output`` command.

* The functional ``testutils`` now correctly check the distinction betweeen ``HIGH`` and
``UNDEFINED`` confidence. Expected output files without defiend ``confidence`` levels will now
trigger a ``DeprecationWarning``. Expected output files can be easily updated with the
``python tests/test_functional.py --update-functional-output`` command.

* Fix ``accept-no-yields-doc`` and ``accept-no-return-doc`` not allowing missing ``yield`` or
``return`` documentation when a docstring is partially correct

Expand Down
9 changes: 9 additions & 0 deletions doc/whatsnew/2.12.rst
Expand Up @@ -123,6 +123,15 @@ Other Changes

Closes #3031

* The functional ``testutils`` now accept ``end_lineno`` and ``end_column``. Expected
output files without these will trigger a ``DeprecationWarning``. Expected output files
can be easily updated with the ``python tests/test_functional.py --update-functional-output`` command.

* The functional ``testutils`` now correctly check the distinction betweeen ``HIGH`` and
``UNDEFINED`` confidence. Expected output files without defiend ``confidence`` levels will now
trigger a ``DeprecationWarning``. Expected output files can be easily updated with the
``python tests/test_functional.py --update-functional-output`` command.

* ``undefined-variable`` now correctly flags variables which only receive a type annotations
and never get assigned a value

Expand Down
95 changes: 88 additions & 7 deletions pylint/testutils/output_line.py
@@ -1,15 +1,18 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE

from typing import Any, NamedTuple, Optional, Sequence, Tuple, Union
import warnings
from typing import Any, NamedTuple, Optional, Sequence, Tuple, TypeVar, Union

from astroid import nodes

from pylint.constants import PY38_PLUS
from pylint.interfaces import HIGH, UNDEFINED, Confidence
from pylint.interfaces import UNDEFINED, Confidence
from pylint.message.message import Message
from pylint.testutils.constants import UPDATE_OPTION

T = TypeVar("T")


class MessageTest(NamedTuple):
msg_id: str
Expand All @@ -33,6 +36,8 @@ def __init__(
"symbol",
"line",
"column",
"end_line",
"end_column",
"MyClass.myFunction, (or '')",
"Message",
"confidence",
Expand Down Expand Up @@ -61,41 +66,117 @@ class OutputLine(NamedTuple):
symbol: str
lineno: int
column: int
end_lineno: Optional[int]
end_column: Optional[int]
object: str
msg: str
confidence: str

@classmethod
def from_msg(cls, msg: Message) -> "OutputLine":
"""Create an OutputLine from a Pylint Message"""
column = cls._get_column(msg.column)
end_line = cls._get_py38_none_value(msg.end_line)
end_column = cls._get_py38_none_value(msg.end_column)
return cls(
msg.symbol,
msg.line,
column,
end_line,
end_column,
msg.obj or "",
msg.msg.replace("\r\n", "\n"),
msg.confidence.name if msg.confidence != UNDEFINED else HIGH.name,
msg.confidence.name,
)

@staticmethod
def _get_column(column: str) -> int:
"""Handle column numbers with the exception of pylint < 3.8 not having them
in the ast parser.
"""
if not PY38_PLUS:
# We check the column only for the new better ast parser introduced in python 3.8
return 0 # pragma: no cover
return int(column)

@staticmethod
def _get_py38_none_value(value: T) -> Optional[T]:
"""Handle attributes that are always None on pylint < 3.8 similar to _get_column."""
if not PY38_PLUS:
# We check the value only for the new better ast parser introduced in python 3.8
return None # pragma: no cover
return value
Comment on lines +102 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will likely not work for nodes that added end_lineno and end_col_offset later. E.g. keyword, Slice (both only in 3.9). Maybe only compare the values if end_lineno is not None or better a new field in [testoptions] to overwrite PY38_PLUS. I.e. if field is specified, ignore end_lineno check for all versions prior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever create messages on those nodes though? Even invalid-slice-index does not pass an nodes.Slice as its node parameter. Could it be that we're fine if we ignore these?

I'm not sure how the testoptions stuff work so I'll need some guidance if want to add a new one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only compare the values if end_lineno is not None is the simpler because I don't know what we could put in the test_option. A condition that needs to succeed or we return None ? Seems like we'll have to launch an eval for that (with all the implication for job launched from fork via github actions) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it a bit more and I think min_pyver_end_line could work? If we convert it to a boolean and pass it to _get_py38_none_value that should make it future proof while testing on versions that it should.

only compare the values if end_lineno is not None is what was previously done for confidence with the __eq__, I don't think that is the way to go as it allows to just never check it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

min_pyver_end_line

Ho, of course. I did not think of that. Very nice solution ! Should we add one option removing the check for line/end_line/column/end_column (min_pyver_position to check all 4 values properly only in the version that is the most accurate) or should we add 4 options (min_pyver_end_line, min_pyver_end_column ... in order to check line/column if line_end or column_end do not exists in other version) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cdce8p Knows more about this than I do so I'll let him respond to this but I believe 3.6 has line and col_offset for all nodes that should have it. 3.8 adds end_line and end_col_offset for most and 3.9 adds it for some that were missing.

Thus I think we can create one min_pyver_end_position (?) which defaults to 3.8.0 and can be increased if needed.

Btw, I'm going to be quite busy tonight and tomorrow probably so if we want to release 2.12 today/tomorrow somebody else would likely need to start working on this. Sorry about that!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry 😄 min_pyver_end_position convinced me, do you agree @cdce8p ? By the way it's probably not strictly necessary to release 2.12.0 as we don't have trouble with it right now. We're probably never emitting message for Slice/Keyword nodes ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found some time, see #5386


@classmethod
def from_csv(cls, row: Union[Sequence[str], str]) -> "OutputLine":
"""Create an OutputLine from a comma separated list (the functional tests expected
output .txt files).
"""
try:
if isinstance(row, Sequence):
column = cls._get_column(row[2])
if len(row) == 5:
return cls(row[0], int(row[1]), column, row[3], row[4], HIGH.name)
warnings.warn(
"In pylint 3.0 functional tests expected output should always include the "
"expected confidence level, expected end_line and expected end_column. "
"An OutputLine should thus have a length of 8.",
DeprecationWarning,
)
return cls(
row[0],
int(row[1]),
column,
None,
None,
row[3],
row[4],
UNDEFINED.name,
)
if len(row) == 6:
return cls(row[0], int(row[1]), column, row[3], row[4], row[5])
warnings.warn(
"In pylint 3.0 functional tests expected output should always include the "
"expected end_line and expected end_column. An OutputLine should thus have "
"a length of 8.",
DeprecationWarning,
)
return cls(
row[0], int(row[1]), column, None, None, row[3], row[4], row[5]
)
if len(row) == 8:
end_line = cls._get_py38_none_value(row[3])
end_column = cls._get_py38_none_value(row[4])
return cls(
row[0],
int(row[1]),
column,
cls._value_to_optional_int(end_line),
cls._value_to_optional_int(end_column),
row[5],
row[6],
row[7],
)
raise IndexError
except Exception as e:
raise MalformedOutputLineException(row, e) from e

def to_csv(self) -> Tuple[str, str, str, str, str, str]:
return tuple(str(i) for i in self) # type: ignore[return-value] # pylint: disable=not-an-iterable
def to_csv(self) -> Tuple[str, str, str, str, str, str, str, str]:
"""Convert an OutputLine to a tuple of string to be written by a
csv-writer.
"""
return (
str(self.symbol),
str(self.lineno),
str(self.column),
str(self.end_lineno),
str(self.end_column),
str(self.object),
str(self.msg),
str(self.confidence),
)

@staticmethod
def _value_to_optional_int(value: Optional[str]) -> Optional[int]:
"""Checks if a (stringified) value should be None or a Python integer"""
if value == "None" or not value:
return None
return int(value)
12 changes: 6 additions & 6 deletions tests/functional/a/abstract/abstract_class_instantiated.txt
@@ -1,6 +1,6 @@
abstract-class-instantiated:108:4:main:Abstract class 'BadMroAbstractMethods' with abstract methods instantiated:HIGH
abstract-class-instantiated:109:4:main:Abstract class 'BadClass' with abstract methods instantiated:HIGH
abstract-class-instantiated:110:4:main:Abstract class 'SecondBadClass' with abstract methods instantiated:HIGH
abstract-class-instantiated:111:4:main:Abstract class 'ThirdBadClass' with abstract methods instantiated:HIGH
abstract-class-instantiated:128:4:main2:Abstract class 'FourthBadClass' with abstract methods instantiated:HIGH
abstract-class-instantiated:143:4:main_two:Abstract class 'BadClassTwo' with abstract methods instantiated:HIGH
abstract-class-instantiated:108:4:108:27:main:Abstract class 'BadMroAbstractMethods' with abstract methods instantiated:UNDEFINED
abstract-class-instantiated:109:4:109:14:main:Abstract class 'BadClass' with abstract methods instantiated:UNDEFINED
abstract-class-instantiated:110:4:110:20:main:Abstract class 'SecondBadClass' with abstract methods instantiated:UNDEFINED
abstract-class-instantiated:111:4:111:19:main:Abstract class 'ThirdBadClass' with abstract methods instantiated:UNDEFINED
abstract-class-instantiated:128:4:128:20:main2:Abstract class 'FourthBadClass' with abstract methods instantiated:UNDEFINED
abstract-class-instantiated:143:4:143:17:main_two:Abstract class 'BadClassTwo' with abstract methods instantiated:UNDEFINED
32 changes: 16 additions & 16 deletions tests/functional/a/abstract/abstract_method.txt
@@ -1,16 +1,16 @@
abstract-method:47:0:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden
abstract-method:70:0:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden
abstract-method:70:0:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden
abstract-method:70:0:Container:Method '__len__' is abstract in class 'Structure' but is not overridden
abstract-method:76:0:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden
abstract-method:76:0:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden
abstract-method:76:0:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden
abstract-method:82:0:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden
abstract-method:82:0:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden
abstract-method:82:0:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden
abstract-method:87:0:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden
abstract-method:87:0:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden
abstract-method:87:0:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden
abstract-method:106:0:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden
abstract-method:106:0:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden
abstract-method:106:0:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden
abstract-method:47:0:51:38:Concrete:Method 'bbbb' is abstract in class 'Abstract' but is not overridden:UNDEFINED
abstract-method:70:0:72:12:Container:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:70:0:72:12:Container:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:70:0:72:12:Container:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:76:0:78:17:Sizable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:76:0:78:17:Sizable:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:76:0:78:17:Sizable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:82:0:83:17:Hashable:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:82:0:83:17:Hashable:Method '__iter__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:82:0:83:17:Hashable:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:87:0:91:19:Iterator:Method '__contains__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:87:0:91:19:Iterator:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:87:0:91:19:Iterator:Method '__len__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:106:0:107:8:BadComplexMro:Method '__hash__' is abstract in class 'Structure' but is not overridden:UNDEFINED
abstract-method:106:0:107:8:BadComplexMro:Method '__len__' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED
abstract-method:106:0:107:8:BadComplexMro:Method 'length' is abstract in class 'AbstractSizable' but is not overridden:UNDEFINED
4 changes: 2 additions & 2 deletions tests/functional/a/access/access_member_before_definition.txt
@@ -1,2 +1,2 @@
access-member-before-definition:8:15:Aaaa.__init__:Access to member '_var2' before its definition line 9
access-member-before-definition:28:19:Bbbb.catchme:Access to member '_repo' before its definition line 30
access-member-before-definition:8:15:8:25:Aaaa.__init__:Access to member '_var2' before its definition line 9:UNDEFINED
access-member-before-definition:28:19:28:29:Bbbb.catchme:Access to member '_repo' before its definition line 30:UNDEFINED
4 changes: 2 additions & 2 deletions tests/functional/a/access/access_to__name__.txt
@@ -1,2 +1,2 @@
no-member:10:14:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member:INFERENCE
no-member:21:22:NewClass.__init__:Instance of 'NewClass' has no '__name__' member:INFERENCE
no-member:10:14:10:27:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member:INFERENCE
no-member:21:22:21:35:NewClass.__init__:Instance of 'NewClass' has no '__name__' member:INFERENCE
56 changes: 28 additions & 28 deletions tests/functional/a/access/access_to_protected_members.txt
@@ -1,28 +1,28 @@
protected-access:19:14:MyClass.test:Access to a protected member _haha of a client class:HIGH
protected-access:41:0::Access to a protected member _protected of a client class:HIGH
protected-access:42:6::Access to a protected member _protected of a client class:HIGH
protected-access:43:0::Access to a protected member _cls_protected of a client class:HIGH
protected-access:44:6::Access to a protected member _cls_protected of a client class:HIGH
protected-access:58:19:Issue1031.incorrect_access:Access to a protected member _protected of a client class:HIGH
protected-access:72:48:Issue1802.__eq__:Access to a protected member __private of a client class:HIGH
protected-access:80:32:Issue1802.not_in_special:Access to a protected member _foo of a client class:HIGH
protected-access:100:32:Issue1802.__fake_special__:Access to a protected member _foo of a client class:HIGH
protected-access:162:8:Issue1159.access_other_attr:Access to a protected member _bar of a client class:HIGH
protected-access:163:12:Issue1159.access_other_attr:Access to a protected member _foo of a client class:HIGH
no-member:194:12:Issue1159Subclass.access_missing_member:Instance of 'Issue1159Subclass' has no '_baz' member; maybe '_bar'?:INFERENCE
protected-access:194:12:Issue1159Subclass.access_missing_member:Access to a protected member _baz of a client class:HIGH
attribute-defined-outside-init:203:8:Issue1159Subclass.assign_missing_member:Attribute '_qux' defined outside __init__:HIGH
protected-access:212:8:Issue1159Subclass.access_other_attr:Access to a protected member _bar of a client class:HIGH
protected-access:213:12:Issue1159Subclass.access_other_attr:Access to a protected member _foo of a client class:HIGH
protected-access:232:8:Issue3066.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:233:8:Issue3066.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:236:8:Issue3066.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:237:8:Issue3066.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:247:12:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:249:12:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:251:12:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:253:12:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:267:16:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:268:16:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:HIGH
protected-access:271:16:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:272:16:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:HIGH
protected-access:19:14:19:31:MyClass.test:Access to a protected member _haha of a client class:UNDEFINED
protected-access:41:0:41:15::Access to a protected member _protected of a client class:UNDEFINED
protected-access:42:6:42:21::Access to a protected member _protected of a client class:UNDEFINED
protected-access:43:0:43:19::Access to a protected member _cls_protected of a client class:UNDEFINED
protected-access:44:6:44:25::Access to a protected member _cls_protected of a client class:UNDEFINED
protected-access:58:19:58:40:Issue1031.incorrect_access:Access to a protected member _protected of a client class:UNDEFINED
protected-access:72:48:72:63:Issue1802.__eq__:Access to a protected member __private of a client class:UNDEFINED
protected-access:80:32:80:42:Issue1802.not_in_special:Access to a protected member _foo of a client class:UNDEFINED
protected-access:100:32:100:42:Issue1802.__fake_special__:Access to a protected member _foo of a client class:UNDEFINED
protected-access:162:8:162:21:Issue1159.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED
protected-access:163:12:163:25:Issue1159.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED
no-member:194:12:194:25:Issue1159Subclass.access_missing_member:Instance of 'Issue1159Subclass' has no '_baz' member; maybe '_bar'?:INFERENCE
protected-access:194:12:194:25:Issue1159Subclass.access_missing_member:Access to a protected member _baz of a client class:UNDEFINED
attribute-defined-outside-init:203:8:203:21:Issue1159Subclass.assign_missing_member:Attribute '_qux' defined outside __init__:UNDEFINED
protected-access:212:8:212:21:Issue1159Subclass.access_other_attr:Access to a protected member _bar of a client class:UNDEFINED
protected-access:213:12:213:25:Issue1159Subclass.access_other_attr:Access to a protected member _foo of a client class:UNDEFINED
protected-access:232:8:232:30:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:233:8:233:37:Issue3066.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:236:8:236:29:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED
protected-access:237:8:237:36:Issue3066.foobar:Access to a protected member _bar of a client class:UNDEFINED
protected-access:247:12:247:27:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:249:12:249:41:Issue3066.Aclass.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:251:12:251:26:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED
protected-access:253:12:253:40:Issue3066.Aclass.foobar:Access to a protected member _bar of a client class:UNDEFINED
protected-access:267:16:267:31:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:268:16:268:38:Issue3066.Aclass.Bclass.foobar:Access to a protected member _attr of a client class:UNDEFINED
protected-access:271:16:271:30:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED
protected-access:272:16:272:37:Issue3066.Aclass.Bclass.foobar:Access to a protected member _bar of a client class:UNDEFINED