| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408 |
- # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
- # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
- """Classes checker for Python code."""
- from __future__ import annotations
- from collections import defaultdict
- from collections.abc import Callable, Sequence
- from functools import cached_property
- from itertools import chain, zip_longest
- from re import Pattern
- from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias
- import astroid
- import astroid.exceptions
- from astroid import bases, nodes, objects, util
- from astroid.typing import SuccessfulInferenceResult
- from pylint.checkers import BaseChecker, utils
- from pylint.checkers.utils import (
- PYMETHODS,
- class_is_abstract,
- decorated_with,
- decorated_with_property,
- get_outer_class,
- has_known_bases,
- is_attr_private,
- is_attr_protected,
- is_builtin_object,
- is_comprehension,
- is_iterable,
- is_property_setter,
- is_property_setter_or_deleter,
- node_frame_class,
- only_required_for_messages,
- safe_infer,
- unimplemented_abstract_methods,
- uninferable_final_decorators,
- )
- from pylint.interfaces import HIGH, INFERENCE
- from pylint.typing import MessageDefinitionTuple
- if TYPE_CHECKING:
- from pylint.lint.pylinter import PyLinter
- _AccessNodes: TypeAlias = nodes.Attribute | nodes.AssignAttr
- INVALID_BASE_CLASSES = {"bool", "range", "slice", "memoryview"}
- ALLOWED_PROPERTIES = {"bultins.property", "functools.cached_property"}
- BUILTIN_DECORATORS = {"builtins.property", "builtins.classmethod"}
- ASTROID_TYPE_COMPARATORS = {
- nodes.Const: lambda a, b: a.value == b.value,
- nodes.ClassDef: lambda a, b: a.qname == b.qname,
- nodes.Tuple: lambda a, b: a.elts == b.elts,
- nodes.List: lambda a, b: a.elts == b.elts,
- nodes.Dict: lambda a, b: a.items == b.items,
- nodes.Name: lambda a, b: set(a.infer()) == set(b.infer()),
- }
- # Dealing with useless override detection, with regard
- # to parameters vs arguments
- class _CallSignature(NamedTuple):
- args: list[str | None]
- kws: dict[str | None, str | None]
- starred_args: list[str]
- starred_kws: list[str]
- class _ParameterSignature(NamedTuple):
- args: list[str]
- kwonlyargs: list[str]
- varargs: str
- kwargs: str
- def _signature_from_call(call: nodes.Call) -> _CallSignature:
- kws = {}
- args = []
- starred_kws = []
- starred_args = []
- for keyword in call.keywords or []:
- arg, value = keyword.arg, keyword.value
- if arg is None and isinstance(value, nodes.Name):
- # Starred node, and we are interested only in names,
- # otherwise some transformation might occur for the parameter.
- starred_kws.append(value.name)
- elif isinstance(value, nodes.Name):
- kws[arg] = value.name
- else:
- kws[arg] = None
- for arg in call.args:
- match arg:
- case nodes.Starred(value=nodes.Name(name=name)):
- # Positional variadic and a name, otherwise some transformation
- # might have occurred.
- starred_args.append(name)
- case nodes.Name():
- args.append(arg.name)
- case _:
- args.append(None)
- return _CallSignature(args, kws, starred_args, starred_kws)
- def _signature_from_arguments(arguments: nodes.Arguments) -> _ParameterSignature:
- kwarg = arguments.kwarg
- vararg = arguments.vararg
- args = [
- arg.name
- for arg in chain(arguments.posonlyargs, arguments.args)
- if arg.name != "self"
- ]
- kwonlyargs = [arg.name for arg in arguments.kwonlyargs]
- return _ParameterSignature(args, kwonlyargs, vararg, kwarg)
- def _definition_equivalent_to_call(
- definition: _ParameterSignature, call: _CallSignature
- ) -> bool:
- """Check if a definition signature is equivalent to a call."""
- if definition.kwargs:
- if definition.kwargs not in call.starred_kws:
- return False
- elif call.starred_kws:
- return False
- if definition.varargs:
- if definition.varargs not in call.starred_args:
- return False
- elif call.starred_args:
- return False
- if any(kw not in call.kws for kw in definition.kwonlyargs):
- return False
- if definition.args != call.args:
- return False
- # No extra kwargs in call.
- return all(kw in call.args or kw in definition.kwonlyargs for kw in call.kws)
- def _is_trivial_super_delegation(function: nodes.FunctionDef) -> bool:
- """Check whether a function definition is a method consisting only of a
- call to the same function on the superclass.
- """
- if (
- not function.is_method()
- # Adding decorators to a function changes behavior and
- # constitutes a non-trivial change.
- or function.decorators
- ):
- return False
- body = function.body
- if len(body) != 1:
- # Multiple statements, which means this overridden method
- # could do multiple things we are not aware of.
- return False
- statement = body[0]
- if not isinstance(statement, (nodes.Expr, nodes.Return)):
- # Doing something else than what we are interested in.
- return False
- call = statement.value
- match call := statement.value:
- case nodes.Call(func=nodes.Attribute(expr=expr)):
- pass
- case _:
- # Not a super() attribute access.
- return False
- # Anything other than a super call is non-trivial.
- super_call = safe_infer(expr)
- if not isinstance(super_call, objects.Super):
- return False
- # The name should be the same.
- if call.func.attrname != function.name:
- return False
- # Should be a super call with the MRO pointer being the
- # current class and the type being the current instance.
- current_scope = function.parent.scope()
- if not (
- super_call.mro_pointer == current_scope
- and isinstance(super_call.type, astroid.Instance)
- and super_call.type.name == current_scope.name
- ):
- return False
- return True
- # Deal with parameters overriding in two methods.
- def _positional_parameters(method: nodes.FunctionDef) -> list[nodes.AssignName]:
- positional = method.args.args
- if method.is_bound() and method.type in {"classmethod", "method"}:
- positional = positional[1:]
- return positional # type: ignore[no-any-return]
- class _DefaultMissing:
- """Sentinel value for missing arg default, use _DEFAULT_MISSING."""
- _DEFAULT_MISSING = _DefaultMissing()
- def _has_different_parameters_default_value(
- original: nodes.Arguments, overridden: nodes.Arguments
- ) -> bool:
- """Check if original and overridden methods arguments have different default values.
- Return True if one of the overridden arguments has a default
- value different from the default value of the original argument
- If one of the method doesn't have argument (.args is None)
- return False
- """
- if original.args is None or overridden.args is None:
- return False
- for param in chain(original.args, original.kwonlyargs):
- try:
- original_default = original.default_value(param.name)
- except astroid.exceptions.NoDefault:
- original_default = _DEFAULT_MISSING
- try:
- overridden_default = overridden.default_value(param.name)
- if original_default is _DEFAULT_MISSING:
- # Only the original has a default.
- return True
- except astroid.exceptions.NoDefault:
- if original_default is _DEFAULT_MISSING:
- # Both have a default, no difference
- continue
- # Only the override has a default.
- return True
- original_type = type(original_default)
- if not isinstance(overridden_default, original_type):
- # Two args with same name but different types
- return True
- is_same_fn: Callable[[Any, Any], bool] | None = ASTROID_TYPE_COMPARATORS.get(
- original_type
- )
- if is_same_fn is None:
- # If the default value comparison is unhandled, assume the value is different
- return True
- if not is_same_fn(original_default, overridden_default):
- # Two args with same type but different values
- return True
- return False
- def _has_different_parameters(
- original: list[nodes.AssignName],
- overridden: list[nodes.AssignName],
- dummy_parameter_regex: Pattern[str],
- ) -> list[str]:
- result: list[str] = []
- zipped = zip_longest(original, overridden)
- for original_param, overridden_param in zipped:
- if not overridden_param:
- return ["Number of parameters "]
- if not original_param:
- try:
- overridden_param.parent.default_value(overridden_param.name)
- continue
- except astroid.NoDefault:
- return ["Number of parameters "]
- # check for the arguments' name
- names = [param.name for param in (original_param, overridden_param)]
- if any(dummy_parameter_regex.match(name) for name in names):
- continue
- if original_param.name != overridden_param.name:
- result.append(
- f"Parameter '{original_param.name}' has been renamed "
- f"to '{overridden_param.name}' in"
- )
- return result
- def _has_different_keyword_only_parameters(
- original: list[nodes.AssignName],
- overridden: list[nodes.AssignName],
- ) -> list[str]:
- """Determine if the two methods have different keyword only parameters."""
- original_names = [i.name for i in original]
- overridden_names = [i.name for i in overridden]
- if any(name not in overridden_names for name in original_names):
- return ["Number of parameters "]
- for name in overridden_names:
- if name in original_names:
- continue
- try:
- overridden[0].parent.default_value(name)
- except astroid.NoDefault:
- return ["Number of parameters "]
- return []
- def _different_parameters(
- original: nodes.FunctionDef,
- overridden: nodes.FunctionDef,
- dummy_parameter_regex: Pattern[str],
- ) -> list[str]:
- """Determine if the two methods have different parameters.
- They are considered to have different parameters if:
- * they have different positional parameters, including different names
- * one of the methods is having variadics, while the other is not
- * they have different keyword only parameters.
- """
- output_messages = []
- original_parameters = _positional_parameters(original)
- overridden_parameters = _positional_parameters(overridden)
- # Copy kwonlyargs list so that we don't affect later function linting
- original_kwonlyargs = original.args.kwonlyargs
- # Allow positional/keyword variadic in overridden to match against any
- # positional/keyword argument in original.
- # Keep any arguments that are found separately in overridden to satisfy
- # later tests
- if overridden.args.vararg:
- overridden_names = [v.name for v in overridden_parameters]
- original_parameters = [
- v for v in original_parameters if v.name in overridden_names
- ]
- if overridden.args.kwarg:
- overridden_names = [v.name for v in overridden.args.kwonlyargs]
- original_kwonlyargs = [
- v for v in original.args.kwonlyargs if v.name in overridden_names
- ]
- different_positional = _has_different_parameters(
- original_parameters, overridden_parameters, dummy_parameter_regex
- )
- different_kwonly = _has_different_keyword_only_parameters(
- original_kwonlyargs, overridden.args.kwonlyargs
- )
- if different_kwonly and different_positional:
- if "Number " in different_positional[0] and "Number " in different_kwonly[0]:
- output_messages.append("Number of parameters ")
- output_messages += different_positional[1:]
- output_messages += different_kwonly[1:]
- else:
- output_messages += different_positional
- output_messages += different_kwonly
- else:
- if different_positional:
- output_messages += different_positional
- if different_kwonly:
- output_messages += different_kwonly
- # Arguments will only violate LSP if there are variadics in the original
- # that are then removed from the overridden
- kwarg_lost = original.args.kwarg and not overridden.args.kwarg
- vararg_lost = original.args.vararg and not overridden.args.vararg
- if kwarg_lost or vararg_lost:
- output_messages += ["Variadics removed in"]
- if original.name in PYMETHODS:
- # Ignore the difference for special methods. If the parameter
- # numbers are different, then that is going to be caught by
- # unexpected-special-method-signature.
- # If the names are different, it doesn't matter, since they can't
- # be used as keyword arguments anyway.
- output_messages.clear()
- return output_messages
- def _is_invalid_base_class(cls: nodes.ClassDef) -> bool:
- return cls.name in INVALID_BASE_CLASSES and is_builtin_object(cls)
- def _has_data_descriptor(cls: nodes.ClassDef, attr: str) -> bool:
- attributes = cls.getattr(attr)
- for attribute in attributes:
- try:
- for inferred in attribute.infer():
- if isinstance(inferred, astroid.Instance):
- try:
- inferred.getattr("__get__")
- inferred.getattr("__set__")
- except astroid.NotFoundError:
- continue
- else:
- return True
- except astroid.InferenceError:
- # Can't infer, avoid emitting a false positive in this case.
- return True
- return False
- def _called_in_methods(
- func: nodes.LocalsDictNodeNG,
- klass: nodes.ClassDef,
- methods: Sequence[str],
- ) -> bool:
- """Check if the func was called in any of the given methods,
- belonging to the *klass*.
- Returns True if so, False otherwise.
- """
- if not isinstance(func, nodes.FunctionDef):
- return False
- for method in methods:
- try:
- inferred = klass.getattr(method)
- except astroid.NotFoundError:
- continue
- for infer_method in inferred:
- for call in infer_method.nodes_of_class(nodes.Call):
- try:
- bound = next(call.func.infer())
- except (astroid.InferenceError, StopIteration):
- continue
- if not isinstance(bound, astroid.BoundMethod):
- continue
- func_obj = bound._proxied
- if isinstance(func_obj, astroid.UnboundMethod):
- func_obj = func_obj._proxied
- if func_obj.name == func.name:
- return True
- return False
- def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool:
- """Check if the given attribute *name* is a property in the given *klass*.
- It will look for `property` calls or for functions
- with the given name, decorated by `property` or `property`
- subclasses.
- Returns ``True`` if the name is a property in the given klass,
- ``False`` otherwise.
- """
- try:
- attributes = klass.getattr(name)
- except astroid.NotFoundError:
- return False
- property_name = "builtins.property"
- for attr in attributes:
- if isinstance(attr, util.UninferableBase):
- continue
- try:
- inferred = next(attr.infer())
- except astroid.InferenceError:
- continue
- if isinstance(inferred, nodes.FunctionDef) and decorated_with_property(
- inferred
- ):
- return True
- if inferred.pytype() == property_name:
- return True
- return False
- def _has_same_layout_slots(
- slots: list[nodes.Const | None], assigned_value: nodes.Name
- ) -> bool:
- inferred = next(assigned_value.infer())
- if isinstance(inferred, nodes.ClassDef):
- other_slots = inferred.slots()
- if all(
- first_slot and second_slot and first_slot.value == second_slot.value
- for (first_slot, second_slot) in zip_longest(slots, other_slots)
- ):
- return True
- return False
- MSGS: dict[str, MessageDefinitionTuple] = {
- "F0202": (
- "Unable to check methods signature (%s / %s)",
- "method-check-failed",
- "Used when Pylint has been unable to check methods signature "
- "compatibility for an unexpected reason. Please report this kind "
- "if you don't make sense of it.",
- ),
- "E0202": (
- "An attribute defined in %s line %s hides this method",
- "method-hidden",
- "Used when a class defines a method which is hidden by an "
- "instance attribute from an ancestor class or set by some "
- "client code.",
- ),
- "E0203": (
- "Access to member %r before its definition line %s",
- "access-member-before-definition",
- "Used when an instance member is accessed before it's actually assigned.",
- ),
- "W0201": (
- "Attribute %r defined outside __init__",
- "attribute-defined-outside-init",
- "Used when an instance attribute is defined outside the __init__ method.",
- ),
- "W0212": (
- "Access to a protected member %s of a client class", # E0214
- "protected-access",
- "Used when a protected member (i.e. class member with a name "
- "beginning with an underscore) is accessed outside the class or a "
- "descendant of the class where it's defined.",
- ),
- "W0213": (
- "Flag member %(overlap)s shares bit positions with %(sources)s",
- "implicit-flag-alias",
- "Used when multiple integer values declared within an enum.IntFlag "
- "class share a common bit position.",
- ),
- "E0211": (
- "Method %r has no argument",
- "no-method-argument",
- "Used when a method which should have the bound instance as "
- "first argument has no argument defined.",
- ),
- "E0213": (
- 'Method %r should have "self" as first argument',
- "no-self-argument",
- 'Used when a method has an attribute different the "self" as '
- "first argument. This is considered as an error since this is "
- "a so common convention that you shouldn't break it!",
- ),
- "C0202": (
- "Class method %s should have %s as first argument",
- "bad-classmethod-argument",
- "Used when a class method has a first argument named differently "
- "than the value specified in valid-classmethod-first-arg option "
- '(default to "cls"), recommended to easily differentiate them '
- "from regular instance methods.",
- ),
- "C0203": (
- "Metaclass method %s should have %s as first argument",
- "bad-mcs-method-argument",
- "Used when a metaclass method has a first argument named "
- "differently than the value specified in valid-classmethod-first"
- '-arg option (default to "cls"), recommended to easily '
- "differentiate them from regular instance methods.",
- ),
- "C0204": (
- "Metaclass class method %s should have %s as first argument",
- "bad-mcs-classmethod-argument",
- "Used when a metaclass class method has a first argument named "
- "differently than the value specified in valid-metaclass-"
- 'classmethod-first-arg option (default to "mcs"), recommended to '
- "easily differentiate them from regular instance methods.",
- ),
- "W0211": (
- "Static method with %r as first argument",
- "bad-staticmethod-argument",
- 'Used when a static method has "self" or a value specified in '
- "valid-classmethod-first-arg option or "
- "valid-metaclass-classmethod-first-arg option as first argument.",
- ),
- "W0221": (
- "%s %s %r method",
- "arguments-differ",
- "Used when a method has a different number of arguments than in "
- "the implemented interface or in an overridden method. Extra arguments "
- "with default values are ignored.",
- ),
- "W0222": (
- "Signature differs from %s %r method",
- "signature-differs",
- "Used when a method signature is different than in the "
- "implemented interface or in an overridden method.",
- ),
- "W0223": (
- "Method %r is abstract in class %r but is not overridden in child class %r",
- "abstract-method",
- "Used when an abstract method (i.e. raise NotImplementedError) is "
- "not overridden in concrete class.",
- ),
- "W0231": (
- "__init__ method from base class %r is not called",
- "super-init-not-called",
- "Used when an ancestor class method has an __init__ method "
- "which is not called by a derived class.",
- ),
- "W0233": (
- "__init__ method from a non direct base class %r is called",
- "non-parent-init-called",
- "Used when an __init__ method is called on a class which is not "
- "in the direct ancestors for the analysed class.",
- ),
- "W0246": (
- "Useless parent or super() delegation in method %r",
- "useless-parent-delegation",
- "Used whenever we can detect that an overridden method is useless, "
- "relying on parent or super() delegation to do the same thing as another method "
- "from the MRO.",
- {"old_names": [("W0235", "useless-super-delegation")]},
- ),
- "W0236": (
- "Method %r was expected to be %r, found it instead as %r",
- "invalid-overridden-method",
- "Used when we detect that a method was overridden in a way "
- "that does not match its base class "
- "which could result in potential bugs at runtime.",
- ),
- "W0237": (
- "%s %s %r method",
- "arguments-renamed",
- "Used when a method parameter has a different name than in "
- "the implemented interface or in an overridden method.",
- ),
- "W0238": (
- "Unused private member `%s.%s`",
- "unused-private-member",
- "Emitted when a private member of a class is defined but not used.",
- ),
- "W0239": (
- "Method %r overrides a method decorated with typing.final which is defined in class %r",
- "overridden-final-method",
- "Used when a method decorated with typing.final has been overridden.",
- ),
- "W0240": (
- "Class %r is a subclass of a class decorated with typing.final: %r",
- "subclassed-final-class",
- "Used when a class decorated with typing.final has been subclassed.",
- ),
- "W0244": (
- "Redefined slots %r in subclass",
- "redefined-slots-in-subclass",
- "Used when a slot is re-defined in a subclass.",
- ),
- "W0245": (
- "Super call without brackets",
- "super-without-brackets",
- "Used when a call to super does not have brackets and thus is not an actual "
- "call and does not work as expected.",
- ),
- "E0236": (
- "Invalid object %r in __slots__, must contain only non empty strings",
- "invalid-slots-object",
- "Used when an invalid (non-string) object occurs in __slots__.",
- ),
- "E0237": (
- "Assigning to attribute %r not defined in class slots",
- "assigning-non-slot",
- "Used when assigning to an attribute not defined in the class slots.",
- ),
- "E0238": (
- "Invalid __slots__ object",
- "invalid-slots",
- "Used when an invalid __slots__ is found in class. "
- "Only a string, an iterable or a sequence is permitted.",
- ),
- "E0239": (
- "Inheriting %r, which is not a class.",
- "inherit-non-class",
- "Used when a class inherits from something which is not a class.",
- ),
- "E0240": (
- "Inconsistent method resolution order for class %r",
- "inconsistent-mro",
- "Used when a class has an inconsistent method resolution order.",
- ),
- "E0241": (
- "Duplicate bases for class %r",
- "duplicate-bases",
- "Duplicate use of base classes in derived classes raise TypeErrors.",
- ),
- "E0242": (
- "Value %r in slots conflicts with class variable",
- "class-variable-slots-conflict",
- "Used when a value in __slots__ conflicts with a class variable, property or method.",
- ),
- "E0243": (
- "Invalid assignment to '__class__'. Should be a class definition but got a '%s'",
- "invalid-class-object",
- "Used when an invalid object is assigned to a __class__ property. "
- "Only a class is permitted.",
- ),
- "E0244": (
- 'Extending inherited Enum class "%s"',
- "invalid-enum-extension",
- "Used when a class tries to extend an inherited Enum class. "
- "Doing so will raise a TypeError at runtime.",
- ),
- "E0245": (
- "No such name %r in __slots__",
- "declare-non-slot",
- "Raised when a type annotation on a class is absent from the list of names in __slots__, "
- "and __slots__ does not contain a __dict__ entry.",
- ),
- "R0202": (
- "Consider using a decorator instead of calling classmethod",
- "no-classmethod-decorator",
- "Used when a class method is defined without using the decorator syntax.",
- ),
- "R0203": (
- "Consider using a decorator instead of calling staticmethod",
- "no-staticmethod-decorator",
- "Used when a static method is defined without using the decorator syntax.",
- ),
- "C0205": (
- "Class __slots__ should be a non-string iterable",
- "single-string-used-for-slots",
- "Used when a class __slots__ is a simple string, rather than an iterable.",
- ),
- "R0205": (
- "Class %r inherits from object, can be safely removed from bases in python3",
- "useless-object-inheritance",
- "Used when a class inherit from object, which under python3 is implicit, "
- "hence can be safely removed from bases.",
- ),
- "R0206": (
- "Cannot have defined parameters for properties",
- "property-with-parameters",
- "Used when we detect that a property also has parameters, which are useless, "
- "given that properties cannot be called with additional arguments.",
- ),
- }
- def _scope_default() -> defaultdict[str, list[_AccessNodes]]:
- # It's impossible to nest defaultdicts so we must use a function
- return defaultdict(list)
- class ScopeAccessMap:
- """Store the accessed variables per scope."""
- def __init__(self) -> None:
- self._scopes: defaultdict[
- nodes.ClassDef, defaultdict[str, list[_AccessNodes]]
- ] = defaultdict(_scope_default)
- def set_accessed(self, node: _AccessNodes) -> None:
- """Set the given node as accessed."""
- frame = node_frame_class(node)
- if frame is None:
- # The node does not live in a class.
- return
- self._scopes[frame][node.attrname].append(node)
- def accessed(self, scope: nodes.ClassDef) -> dict[str, list[_AccessNodes]]:
- """Get the accessed variables for the given scope."""
- return self._scopes.get(scope, {})
- class ClassChecker(BaseChecker):
- """Checker for class nodes.
- Checks for :
- * methods without self as first argument
- * overridden methods signature
- * access only to existent members via self
- * attributes not defined in the __init__ method
- * unreachable code
- """
- # configuration section name
- name = "classes"
- # messages
- msgs = MSGS
- # configuration options
- options = (
- (
- "defining-attr-methods",
- {
- "default": (
- "__init__",
- "__new__",
- "setUp",
- "asyncSetUp",
- "__post_init__",
- ),
- "type": "csv",
- "metavar": "<method names>",
- "help": "List of method names used to declare (i.e. assign) \
- instance attributes.",
- },
- ),
- (
- "valid-classmethod-first-arg",
- {
- "default": ("cls",),
- "type": "csv",
- "metavar": "<argument names>",
- "help": "List of valid names for the first argument in \
- a class method.",
- },
- ),
- (
- "valid-metaclass-classmethod-first-arg",
- {
- "default": ("mcs",),
- "type": "csv",
- "metavar": "<argument names>",
- "help": "List of valid names for the first argument in \
- a metaclass class method.",
- },
- ),
- (
- "exclude-protected",
- {
- "default": (
- # namedtuple public API.
- "_asdict",
- "_fields",
- "_replace",
- "_source",
- "_make",
- "os._exit",
- ),
- "type": "csv",
- "metavar": "<protected access exclusions>",
- "help": (
- "List of member names, which should be excluded "
- "from the protected access warning."
- ),
- },
- ),
- (
- "check-protected-access-in-special-methods",
- {
- "default": False,
- "type": "yn",
- "metavar": "<y or n>",
- "help": "Warn about protected attribute access inside special methods",
- },
- ),
- )
- def __init__(self, linter: PyLinter) -> None:
- super().__init__(linter)
- self._accessed = ScopeAccessMap()
- self._first_attrs: list[str | None] = []
- def open(self) -> None:
- self._mixin_class_rgx = self.linter.config.mixin_class_rgx
- py_version = self.linter.config.py_version
- self._py38_plus = py_version >= (3, 8)
- @cached_property
- def _dummy_rgx(self) -> Pattern[str]:
- return self.linter.config.dummy_variables_rgx # type: ignore[no-any-return]
- @only_required_for_messages(
- "abstract-method",
- "invalid-slots",
- "single-string-used-for-slots",
- "invalid-slots-object",
- "class-variable-slots-conflict",
- "inherit-non-class",
- "useless-object-inheritance",
- "inconsistent-mro",
- "duplicate-bases",
- "redefined-slots-in-subclass",
- "invalid-enum-extension",
- "subclassed-final-class",
- "implicit-flag-alias",
- "declare-non-slot",
- )
- def visit_classdef(self, node: nodes.ClassDef) -> None:
- """Init visit variable _accessed."""
- self._check_bases_classes(node)
- self._check_slots(node)
- self._check_proper_bases(node)
- self._check_typing_final(node)
- self._check_consistent_mro(node)
- self._check_declare_non_slot(node)
- def _check_declare_non_slot(self, node: nodes.ClassDef) -> None:
- if not self._has_valid_slots(node):
- return
- slot_names = self._get_classdef_slots_names(node)
- # Stop if empty __slots__ in the class body, this likely indicates that
- # this class takes part in multiple inheritance with other slotted classes.
- if not slot_names:
- return
- # Stop if we find __dict__, since this means attributes can be set
- # dynamically
- if "__dict__" in slot_names:
- return
- for base in node.bases:
- ancestor = safe_infer(base)
- if not isinstance(ancestor, nodes.ClassDef):
- continue
- # if any base doesn't have __slots__, attributes can be set dynamically, so stop
- if not self._has_valid_slots(ancestor):
- return
- for slot_name in self._get_classdef_slots_names(ancestor):
- if slot_name == "__dict__":
- return
- slot_names.append(slot_name)
- # Every class in bases has __slots__, our __slots__ is non-empty and there is no __dict__
- for child in node.body:
- match child:
- case nodes.AnnAssign(
- target=nodes.AssignName(name=name), value=None
- ) if (name not in slot_names):
- self.add_message(
- "declare-non-slot",
- args=child.target.name,
- node=child.target,
- confidence=INFERENCE,
- )
- def _check_consistent_mro(self, node: nodes.ClassDef) -> None:
- """Detect that a class has a consistent mro or duplicate bases."""
- try:
- node.mro()
- except astroid.InconsistentMroError:
- self.add_message("inconsistent-mro", args=node.name, node=node)
- except astroid.DuplicateBasesError:
- self.add_message("duplicate-bases", args=node.name, node=node)
- def _check_enum_base(self, node: nodes.ClassDef, ancestor: nodes.ClassDef) -> None:
- match ancestor.getattr("__members__"):
- case [nodes.Dict(items=items), *_] if items:
- for _, name_node in items:
- # Exempt type annotations without value assignments
- if all(
- isinstance(item.parent, nodes.AnnAssign)
- and item.parent.value is None
- for item in ancestor.getattr(name_node.name)
- ):
- continue
- self.add_message(
- "invalid-enum-extension",
- args=ancestor.name,
- node=node,
- confidence=INFERENCE,
- )
- break
- if ancestor.is_subtype_of("enum.IntFlag"):
- # Collect integer flag assignments present on the class
- assignments = defaultdict(list)
- for assign_name in node.nodes_of_class(nodes.AssignName):
- match assign_name.parent:
- case nodes.Assign(value=object(value=int() as value)):
- assignments[value].append(assign_name)
- # For each bit position, collect all the flags that set the bit
- bit_flags = defaultdict(set)
- for flag in assignments:
- flag_bits = (i for i, c in enumerate(reversed(bin(flag))) if c == "1")
- for bit in flag_bits:
- bit_flags[bit].add(flag)
- # Collect the minimum, unique values that each flag overlaps with
- overlaps = defaultdict(list)
- for flags in bit_flags.values():
- source, *conflicts = sorted(flags)
- for conflict in conflicts:
- overlaps[conflict].append(source)
- # Report the overlapping values
- for overlap in overlaps:
- for assignment_node in assignments[overlap]:
- self.add_message(
- "implicit-flag-alias",
- node=assignment_node,
- args={
- "overlap": f"<{node.name}.{assignment_node.name}: {overlap}>",
- "sources": ", ".join(
- f"<{node.name}.{assignments[source][0].name}: {source}> "
- f"({overlap} & {source} = {overlap & source})"
- for source in overlaps[overlap]
- ),
- },
- confidence=INFERENCE,
- )
- def _check_proper_bases(self, node: nodes.ClassDef) -> None:
- """Detect that a class inherits something which is not
- a class or a type.
- """
- for base in node.bases:
- ancestor = safe_infer(base)
- if not ancestor:
- continue
- if isinstance(ancestor, astroid.Instance) and (
- ancestor.is_subtype_of("builtins.type")
- or ancestor.is_subtype_of(".Protocol")
- ):
- continue
- if not isinstance(ancestor, nodes.ClassDef) or _is_invalid_base_class(
- ancestor
- ):
- self.add_message("inherit-non-class", args=base.as_string(), node=node)
- if isinstance(ancestor, nodes.ClassDef) and ancestor.is_subtype_of(
- "enum.Enum"
- ):
- self._check_enum_base(node, ancestor)
- if ancestor.name == object.__name__:
- self.add_message(
- "useless-object-inheritance", args=node.name, node=node
- )
- def _check_typing_final(self, node: nodes.ClassDef) -> None:
- """Detect that a class does not subclass a class decorated with
- `typing.final`.
- """
- if not self._py38_plus:
- return
- for base in node.bases:
- ancestor = safe_infer(base)
- if not ancestor:
- continue
- if isinstance(ancestor, nodes.ClassDef) and (
- decorated_with(ancestor, ["typing.final"])
- or uninferable_final_decorators(ancestor.decorators)
- ):
- self.add_message(
- "subclassed-final-class",
- args=(node.name, ancestor.name),
- node=node,
- )
- @only_required_for_messages(
- "unused-private-member",
- "attribute-defined-outside-init",
- "access-member-before-definition",
- )
- def leave_classdef(self, node: nodes.ClassDef) -> None:
- """Checker for Class nodes.
- check that instance attributes are defined in __init__ and check
- access to existent members
- """
- self._check_unused_private_functions(node)
- self._check_unused_private_variables(node)
- self._check_unused_private_attributes(node)
- self._check_attribute_defined_outside_init(node)
- def _check_unused_private_functions(self, node: nodes.ClassDef) -> None:
- for function_def in node.nodes_of_class(nodes.FunctionDef):
- if not is_attr_private(function_def.name):
- continue
- parent_scope = function_def.parent.scope()
- if isinstance(parent_scope, nodes.FunctionDef):
- # Handle nested functions
- if function_def.name in (
- n.name for n in parent_scope.nodes_of_class(nodes.Name)
- ):
- continue
- for child in node.nodes_of_class((nodes.Name, nodes.Attribute)):
- # Check for cases where the functions are used as a variable instead of as a
- # method call
- if isinstance(child, nodes.Name) and child.name == function_def.name:
- break
- if isinstance(child, nodes.Attribute):
- # Ignore recursive calls
- if (
- child.attrname != function_def.name
- or child.scope() == function_def
- ):
- continue
- # Check self.__attrname, cls.__attrname, node_name.__attrname
- if isinstance(child.expr, nodes.Name) and child.expr.name in {
- "self",
- "cls",
- node.name,
- }:
- break
- # Check type(self).__attrname
- if isinstance(child.expr, nodes.Call):
- inferred = safe_infer(child.expr)
- if (
- isinstance(inferred, nodes.ClassDef)
- and inferred.name == node.name
- ):
- break
- else:
- name_stack = []
- curr = parent_scope
- # Generate proper names for nested functions
- while curr != node:
- name_stack.append(curr.name)
- curr = curr.parent.scope()
- outer_level_names = f"{'.'.join(reversed(name_stack))}"
- function_repr = f"{outer_level_names}.{function_def.name}({function_def.args.as_string()})"
- self.add_message(
- "unused-private-member",
- node=function_def,
- args=(node.name, function_repr.lstrip(".")),
- )
- def _check_unused_private_variables(self, node: nodes.ClassDef) -> None:
- """Check if private variables are never used within a class."""
- for assign_name in node.nodes_of_class(nodes.AssignName):
- if isinstance(assign_name.parent, nodes.Arguments):
- continue # Ignore function arguments
- if not is_attr_private(assign_name.name):
- continue
- for child in node.nodes_of_class((nodes.Name, nodes.Attribute)):
- if isinstance(child, nodes.Name) and child.name == assign_name.name:
- break
- if isinstance(child, nodes.Attribute):
- if not isinstance(child.expr, nodes.Name):
- break
- if child.attrname == assign_name.name and child.expr.name in (
- "self",
- "cls",
- node.name,
- ):
- break
- else:
- args = (node.name, assign_name.name)
- self.add_message("unused-private-member", node=assign_name, args=args)
- def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None:
- for assign_attr in node.nodes_of_class(nodes.AssignAttr):
- if not (
- is_attr_private(assign_attr.attrname)
- and isinstance(assign_attr.expr, nodes.Name)
- ):
- continue
- # Logic for checking false positive when using __new__,
- # Get the returned object names of the __new__ magic function
- # Then check if the attribute was consumed in other instance methods
- acceptable_obj_names: list[str] = ["self"]
- scope = assign_attr.scope()
- if isinstance(scope, nodes.FunctionDef) and scope.name == "__new__":
- acceptable_obj_names.extend(
- [
- return_node.value.name
- for return_node in scope.nodes_of_class(nodes.Return)
- if isinstance(return_node.value, nodes.Name)
- ]
- )
- for attribute in node.nodes_of_class(nodes.Attribute):
- if attribute.attrname != assign_attr.attrname:
- continue
- if not isinstance(attribute.expr, nodes.Name):
- continue
- if assign_attr.expr.name in {
- "cls",
- node.name,
- } and attribute.expr.name in {"cls", "self", node.name}:
- # If assigned to cls or class name, can be accessed by cls/self/class name
- break
- if (
- assign_attr.expr.name in acceptable_obj_names
- and attribute.expr.name == "self"
- ):
- # If assigned to self.attrib, can only be accessed by self
- # Or if __new__ was used, the returned object names are acceptable
- break
- if assign_attr.expr.name == attribute.expr.name == node.name:
- # Recognise attributes which are accessed via the class name
- break
- else:
- args = (node.name, assign_attr.attrname)
- self.add_message("unused-private-member", node=assign_attr, args=args)
- def _check_attribute_defined_outside_init(self, cnode: nodes.ClassDef) -> None:
- # check access to existent members on non metaclass classes
- if (
- "attribute-defined-outside-init"
- in self.linter.config.ignored_checks_for_mixins
- and self._mixin_class_rgx.match(cnode.name)
- ):
- # We are in a mixin class. No need to try to figure out if
- # something is missing, since it is most likely that it will
- # miss.
- return
- accessed = self._accessed.accessed(cnode)
- if cnode.type != "metaclass":
- self._check_accessed_members(cnode, accessed)
- # checks attributes are defined in an allowed method such as __init__
- if not self.linter.is_message_enabled("attribute-defined-outside-init"):
- return
- defining_methods = self.linter.config.defining_attr_methods
- current_module = cnode.root()
- for attr, nodes_lst in cnode.instance_attrs.items():
- # Exclude `__dict__` as it is already defined.
- if attr == "__dict__":
- continue
- # Skip nodes which are not in the current module and it may screw up
- # the output, while it's not worth it
- nodes_lst = [
- n
- for n in nodes_lst
- if not isinstance(n.statement(), (nodes.Delete, nodes.AugAssign))
- and n.root() is current_module
- ]
- if not nodes_lst:
- continue # error detected by typechecking
- # Check if any method attr is defined in is a defining method
- # or if we have the attribute defined in a setter.
- frames = (node.frame() for node in nodes_lst)
- if any(
- frame.name in defining_methods or is_property_setter(frame)
- for frame in frames
- ):
- continue
- # check attribute is defined in a parent's __init__
- for parent in cnode.instance_attr_ancestors(attr):
- attr_defined = False
- # check if any parent method attr is defined in is a defining method
- for node in parent.instance_attrs[attr]:
- if node.frame().name in defining_methods:
- attr_defined = True
- if attr_defined:
- # we're done :)
- break
- else:
- # check attribute is defined as a class attribute
- try:
- cnode.local_attr(attr)
- except astroid.NotFoundError:
- for node in nodes_lst:
- if node.frame().name not in defining_methods:
- # If the attribute was set by a call in any
- # of the defining methods, then don't emit
- # the warning.
- if _called_in_methods(
- node.frame(), cnode, defining_methods
- ):
- continue
- self.add_message(
- "attribute-defined-outside-init", args=attr, node=node
- )
- # pylint: disable = too-many-branches
- def visit_functiondef(self, node: nodes.FunctionDef) -> None:
- """Check method arguments, overriding."""
- # ignore actual functions
- if not node.is_method():
- return
- self._check_useless_super_delegation(node)
- self._check_property_with_parameters(node)
- # 'is_method()' is called and makes sure that this is a 'nodes.ClassDef'
- klass: nodes.ClassDef = node.parent.frame()
- # check first argument is self if this is actually a method
- self._check_first_arg_for_type(node, klass.type == "metaclass")
- if node.name == "__init__":
- self._check_init(node, klass)
- return
- # check signature if the method overloads inherited method
- for overridden in klass.local_attr_ancestors(node.name):
- # get astroid for the searched method
- try:
- parent_function = overridden[node.name]
- except KeyError:
- # we have found the method but it's not in the local
- # dictionary.
- # This may happen with astroid build from living objects
- continue
- if not isinstance(parent_function, nodes.FunctionDef):
- continue
- self._check_signature(node, parent_function, klass)
- self._check_invalid_overridden_method(node, parent_function)
- break
- if node.decorators:
- for decorator in node.decorators.nodes:
- match decorator:
- case nodes.Attribute(attrname="getter" | "setter" | "deleter"):
- # attribute affectation will call this method, not hiding it
- return
- case nodes.Name():
- if decorator.name in ALLOWED_PROPERTIES:
- # attribute affectation will either call a setter or raise
- # an attribute error, anyway not hiding the function
- return
- case nodes.Attribute():
- if self._check_functools_or_not(decorator):
- return
- # Infer the decorator and see if it returns something useful
- inferred = safe_infer(decorator)
- if not inferred:
- return
- if isinstance(inferred, nodes.FunctionDef):
- # Okay, it's a decorator, let's see what it can infer.
- try:
- inferred = next(inferred.infer_call_result(inferred))
- except astroid.InferenceError:
- return
- try:
- if (
- isinstance(inferred, (astroid.Instance, nodes.ClassDef))
- and inferred.getattr("__get__")
- and inferred.getattr("__set__")
- ):
- return
- except astroid.AttributeInferenceError:
- pass
- # check if the method is hidden by an attribute
- # pylint: disable = too-many-try-statements
- try:
- overridden = klass.instance_attr(node.name)[0]
- overridden_frame = overridden.frame()
- match overridden_frame:
- case nodes.FunctionDef(type="method"):
- overridden_frame = overridden_frame.parent.frame()
- if not (
- isinstance(overridden_frame, nodes.ClassDef)
- and klass.is_subtype_of(overridden_frame.qname())
- ):
- return
- # If a subclass defined the method then it's not our fault.
- for ancestor in klass.ancestors():
- if node.name in ancestor.instance_attrs and is_attr_private(node.name):
- return
- for obj in ancestor.lookup(node.name)[1]:
- if isinstance(obj, nodes.FunctionDef):
- return
- args = (overridden.root().name, overridden.fromlineno)
- self.add_message("method-hidden", args=args, node=node)
- except astroid.NotFoundError:
- pass
- visit_asyncfunctiondef = visit_functiondef
- def _check_useless_super_delegation(self, function: nodes.FunctionDef) -> None:
- """Check if the given function node is an useless method override.
- We consider it *useless* if it uses the super() builtin, but having
- nothing additional whatsoever than not implementing the method at all.
- If the method uses super() to delegate an operation to the rest of the MRO,
- and if the method called is the same as the current one, the arguments
- passed to super() are the same as the parameters that were passed to
- this method, then the method could be removed altogether, by letting
- other implementation to take precedence.
- """
- if not _is_trivial_super_delegation(function):
- return
- call: nodes.Call = function.body[0].value
- # Classes that override __eq__ should also override
- # __hash__, even a trivial override is meaningful
- if function.name == "__hash__":
- for other_method in function.parent.mymethods():
- if other_method.name == "__eq__":
- return
- # Check values of default args
- klass = function.parent.frame()
- meth_node = None
- for overridden in klass.local_attr_ancestors(function.name):
- # get astroid for the searched method
- try:
- meth_node = overridden[function.name]
- except KeyError:
- # we have found the method but it's not in the local
- # dictionary.
- # This may happen with astroid build from living objects
- continue
- if (
- not isinstance(meth_node, nodes.FunctionDef)
- # If the method have an ancestor which is not a
- # function then it is legitimate to redefine it
- or _has_different_parameters_default_value(
- meth_node.args, function.args
- )
- # arguments to builtins such as Exception.__init__() cannot be inspected
- or (meth_node.args.args is None and function.argnames() != ["self"])
- ):
- return
- break
- # Detect if the parameters are the same as the call's arguments.
- params = _signature_from_arguments(function.args)
- args = _signature_from_call(call)
- if meth_node is not None:
- # Detect if the super method uses varargs and the function doesn't or makes some of those explicit
- if meth_node.args.vararg and (
- not function.args.vararg
- or len(function.args.args) > len(meth_node.args.args)
- ):
- return
- def form_annotations(arguments: nodes.Arguments) -> list[str]:
- annotations = chain(
- (arguments.posonlyargs_annotations or []), arguments.annotations
- )
- return [ann.as_string() for ann in annotations if ann is not None]
- called_annotations = form_annotations(function.args)
- overridden_annotations = form_annotations(meth_node.args)
- if called_annotations and overridden_annotations:
- if called_annotations != overridden_annotations:
- return
- if (
- function.returns is not None
- and meth_node.returns is not None
- and meth_node.returns.as_string() != function.returns.as_string()
- ):
- # Override adds typing information to the return type
- return
- if _definition_equivalent_to_call(params, args):
- self.add_message(
- "useless-parent-delegation",
- node=function,
- args=(function.name,),
- confidence=INFERENCE,
- )
- def _check_property_with_parameters(self, node: nodes.FunctionDef) -> None:
- if (
- len(node.args.arguments) > 1
- and decorated_with_property(node)
- and not is_property_setter(node)
- ):
- self.add_message("property-with-parameters", node=node, confidence=HIGH)
- def _check_invalid_overridden_method(
- self,
- function_node: nodes.FunctionDef,
- parent_function_node: nodes.FunctionDef,
- ) -> None:
- parent_is_property = decorated_with_property(
- parent_function_node
- ) or is_property_setter_or_deleter(parent_function_node)
- current_is_property = decorated_with_property(
- function_node
- ) or is_property_setter_or_deleter(function_node)
- if parent_is_property and not current_is_property:
- self.add_message(
- "invalid-overridden-method",
- args=(function_node.name, "property", function_node.type),
- node=function_node,
- )
- elif not parent_is_property and current_is_property:
- self.add_message(
- "invalid-overridden-method",
- args=(function_node.name, "method", "property"),
- node=function_node,
- )
- parent_is_async = isinstance(parent_function_node, nodes.AsyncFunctionDef)
- current_is_async = isinstance(function_node, nodes.AsyncFunctionDef)
- if parent_is_async and not current_is_async:
- self.add_message(
- "invalid-overridden-method",
- args=(function_node.name, "async", "non-async"),
- node=function_node,
- )
- elif not parent_is_async and current_is_async:
- self.add_message(
- "invalid-overridden-method",
- args=(function_node.name, "non-async", "async"),
- node=function_node,
- )
- if (
- decorated_with(parent_function_node, ["typing.final"])
- or uninferable_final_decorators(parent_function_node.decorators)
- ) and self._py38_plus:
- self.add_message(
- "overridden-final-method",
- args=(function_node.name, parent_function_node.parent.frame().name),
- node=function_node,
- )
- def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool:
- if decorator.attrname != "cached_property":
- return False
- if not isinstance(decorator.expr, nodes.Name):
- return False
- _, import_nodes = decorator.expr.lookup(decorator.expr.name)
- if not import_nodes:
- return False
- import_node = import_nodes[0]
- if not isinstance(import_node, (nodes.Import, nodes.ImportFrom)):
- return False
- return "functools" in dict(import_node.names)
- def _has_valid_slots(self, node: nodes.ClassDef) -> bool:
- if "__slots__" not in node.locals:
- return False
- try:
- inferred_slots = tuple(node.ilookup("__slots__"))
- except astroid.InferenceError:
- return False
- for slots in inferred_slots:
- # check if __slots__ is a valid type
- if isinstance(slots, util.UninferableBase):
- return False
- if not is_iterable(slots) and not is_comprehension(slots):
- return False
- if isinstance(slots, nodes.Const):
- return False
- if not hasattr(slots, "itered"):
- # we can't obtain the values, maybe a .deque?
- return False
- return True
- def _check_slots(self, node: nodes.ClassDef) -> None:
- if "__slots__" not in node.locals:
- return
- try:
- inferred_slots = tuple(node.ilookup("__slots__"))
- except astroid.InferenceError:
- return
- for slots in inferred_slots:
- # check if __slots__ is a valid type
- if isinstance(slots, util.UninferableBase):
- continue
- if not is_iterable(slots) and not is_comprehension(slots):
- self.add_message("invalid-slots", node=node)
- continue
- if isinstance(slots, nodes.Const):
- # a string, ignore the following checks
- self.add_message("single-string-used-for-slots", node=node)
- continue
- if not hasattr(slots, "itered"):
- # we can't obtain the values, maybe a .deque?
- continue
- if isinstance(slots, nodes.Dict):
- values = [item[0] for item in slots.items]
- else:
- values = slots.itered()
- if isinstance(values, util.UninferableBase):
- continue
- for elt in values:
- try:
- self._check_slots_elt(elt, node)
- except astroid.InferenceError:
- continue
- self._check_redefined_slots(node, slots, values)
- def _get_classdef_slots_names(self, node: nodes.ClassDef) -> list[str]:
- slots_names: list[str] = []
- try:
- inferred_slots = tuple(node.ilookup("__slots__"))
- except astroid.InferenceError: # pragma: no cover
- return slots_names
- for slots in inferred_slots:
- if isinstance(slots, nodes.Dict):
- values = [item[0] for item in slots.items]
- else:
- values = slots.itered()
- slots_names.extend(self._get_slots_names(values))
- return slots_names
- def _get_slots_names(self, slots_list: list[nodes.NodeNG]) -> list[str]:
- slots_names: list[str] = []
- for slot in slots_list:
- if isinstance(slot, nodes.Const):
- slots_names.append(slot.value)
- else:
- inferred_slot = safe_infer(slot)
- inferred_slot_value = getattr(inferred_slot, "value", None)
- if isinstance(inferred_slot_value, str):
- slots_names.append(inferred_slot_value)
- return slots_names
- def _check_redefined_slots(
- self,
- node: nodes.ClassDef,
- slots_node: nodes.NodeNG,
- slots_list: list[nodes.NodeNG],
- ) -> None:
- """Check if `node` redefines a slot which is defined in an ancestor class."""
- slots_names: list[str] = self._get_slots_names(slots_list)
- # Slots of all parent classes
- ancestors_slots_names = {
- slot.value
- for ancestor in node.local_attr_ancestors("__slots__")
- for slot in ancestor.slots() or []
- }
- # Slots which are common to `node` and its parent classes
- redefined_slots = ancestors_slots_names.intersection(slots_names)
- if redefined_slots:
- self.add_message(
- "redefined-slots-in-subclass",
- args=([name for name in slots_names if name in redefined_slots],),
- node=slots_node,
- )
- def _check_slots_elt(
- self, elt: SuccessfulInferenceResult, node: nodes.ClassDef
- ) -> None:
- for inferred in elt.infer():
- match inferred:
- case util.UninferableBase():
- continue
- case nodes.Const(value=str() as value) if value:
- pass
- case _:
- self.add_message(
- "invalid-slots-object",
- args=elt.as_string(),
- node=elt,
- confidence=INFERENCE,
- )
- continue
- # Check if we have a conflict with a class variable.
- match class_variable := node.locals.get(inferred.value):
- case [nodes.NodeNG(parent=nodes.AnnAssign(value=None))]:
- # Skip annotated assignments which don't conflict at all with slots.
- return
- case _ if class_variable:
- self.add_message(
- "class-variable-slots-conflict",
- args=(inferred.value,),
- node=elt,
- )
- def leave_functiondef(self, node: nodes.FunctionDef) -> None:
- """On method node, check if this method couldn't be a function.
- ignore class, static and abstract methods, initializer,
- methods overridden from a parent class.
- """
- if node.is_method():
- if node.args.args is not None:
- self._first_attrs.pop()
- leave_asyncfunctiondef = leave_functiondef
- def visit_attribute(self, node: nodes.Attribute) -> None:
- """Check if the getattr is an access to a class member
- if so, register it.
- Also check for access to protected
- class member from outside its class (but ignore __special__
- methods)
- """
- self._check_super_without_brackets(node)
- # Check self
- if self._uses_mandatory_method_param(node):
- self._accessed.set_accessed(node)
- return
- if not self.linter.is_message_enabled("protected-access"):
- return
- self._check_protected_attribute_access(node)
- def _check_super_without_brackets(self, node: nodes.Attribute) -> None:
- """Check if there is a function call on a super call without brackets."""
- # Check if attribute call is in frame definition in class definition
- frame = node.frame()
- if not isinstance(frame, nodes.FunctionDef):
- return
- if not isinstance(frame.parent.frame(), nodes.ClassDef):
- return
- if not isinstance(node.parent, nodes.Call):
- return
- if not isinstance(node.expr, nodes.Name):
- return
- if node.expr.name == "super":
- self.add_message("super-without-brackets", node=node.expr, confidence=HIGH)
- @only_required_for_messages(
- "assigning-non-slot", "invalid-class-object", "access-member-before-definition"
- )
- def visit_assignattr(self, node: nodes.AssignAttr) -> None:
- if isinstance(
- node.assign_type(), nodes.AugAssign
- ) and self._uses_mandatory_method_param(node):
- self._accessed.set_accessed(node)
- self._check_in_slots(node)
- self._check_invalid_class_object(node)
- def _check_invalid_class_object(self, node: nodes.AssignAttr) -> None:
- if not node.attrname == "__class__":
- return
- if isinstance(node.parent, nodes.Tuple):
- class_index = -1
- for i, elt in enumerate(node.parent.elts):
- if hasattr(elt, "attrname") and elt.attrname == "__class__":
- class_index = i
- if class_index == -1:
- # This should not happen because we checked that the node name
- # is '__class__' earlier, but let's not be too confident here
- return # pragma: no cover
- inferred = safe_infer(node.parent.parent.value.elts[class_index])
- else:
- inferred = safe_infer(node.parent.value)
- match inferred:
- case nodes.ClassDef() | util.UninferableBase() | None:
- # If uninferable, we allow it to prevent false positives
- return
- self.add_message(
- "invalid-class-object",
- node=node,
- args=inferred.__class__.__name__,
- confidence=INFERENCE,
- )
- def _check_in_slots(self, node: nodes.AssignAttr) -> None:
- """Check that the given AssignAttr node
- is defined in the class slots.
- """
- inferred = safe_infer(node.expr)
- if not isinstance(inferred, astroid.Instance):
- return
- klass = inferred._proxied
- if not has_known_bases(klass):
- return
- if "__slots__" not in klass.locals:
- return
- # If `__setattr__` is defined on the class, then we can't reason about
- # what will happen when assigning to an attribute.
- if any(
- base.locals.get("__setattr__")
- for base in klass.mro()
- if base.qname() != "builtins.object"
- ):
- return
- # If 'typing.Generic' is a base of bases of klass, the cached version
- # of 'slots()' might have been evaluated incorrectly, thus deleted cache entry.
- if any(base.qname() == "typing.Generic" for base in klass.mro()):
- cache = getattr(klass, "__cache", None)
- if cache and cache.get(klass.slots) is not None:
- del cache[klass.slots]
- slots = klass.slots()
- if slots is None:
- return
- # If any ancestor doesn't use slots, the slots
- # defined for this class are superfluous.
- if any(
- "__slots__" not in ancestor.locals
- and ancestor.name not in ("Generic", "object")
- for ancestor in klass.ancestors()
- ):
- return
- if not any(slot.value == node.attrname for slot in slots):
- # If we have a '__dict__' in slots, then
- # assigning any name is valid.
- if not any(slot.value == "__dict__" for slot in slots):
- if _is_attribute_property(node.attrname, klass):
- # Properties circumvent the slots mechanism,
- # so we should not emit a warning for them.
- return
- if node.attrname != "__class__" and utils.is_class_attr(
- node.attrname, klass
- ):
- return
- if node.attrname in klass.locals:
- for local_name in klass.locals.get(node.attrname):
- statement = local_name.statement()
- if (
- isinstance(statement, nodes.AnnAssign)
- and not statement.value
- ):
- return
- if _has_data_descriptor(klass, node.attrname):
- # Descriptors circumvent the slots mechanism as well.
- return
- if node.attrname == "__class__" and _has_same_layout_slots(
- slots, node.parent.value
- ):
- return
- self.add_message(
- "assigning-non-slot",
- args=(node.attrname,),
- node=node,
- confidence=INFERENCE,
- )
- @only_required_for_messages(
- "protected-access", "no-classmethod-decorator", "no-staticmethod-decorator"
- )
- def visit_assign(self, assign_node: nodes.Assign) -> None:
- self._check_classmethod_declaration(assign_node)
- node = assign_node.targets[0]
- if not isinstance(node, nodes.AssignAttr):
- return
- if self._uses_mandatory_method_param(node):
- return
- self._check_protected_attribute_access(node)
- def _check_classmethod_declaration(self, node: nodes.Assign) -> None:
- """Checks for uses of classmethod() or staticmethod().
- When a @classmethod or @staticmethod decorator should be used instead.
- A message will be emitted only if the assignment is at a class scope
- and only if the classmethod's argument belongs to the class where it
- is defined.
- `node` is an assign node.
- """
- # check the function called is "classmethod" or "staticmethod"
- # Check if the arg passed to classmethod is a class member
- match node.value:
- case nodes.Call(
- func=nodes.Name(name="classmethod" | "staticmethod" as name),
- args=[nodes.Name(name=method_name), *_],
- ):
- pass
- case _:
- return
- msg = (
- "no-classmethod-decorator"
- if name == "classmethod"
- else "no-staticmethod-decorator"
- )
- # assignment must be at a class scope
- parent_class = node.scope()
- if not isinstance(parent_class, nodes.ClassDef):
- return
- if any(method_name == member.name for member in parent_class.mymethods()):
- self.add_message(msg, node=node.targets[0])
- def _check_protected_attribute_access(
- self, node: nodes.Attribute | nodes.AssignAttr
- ) -> None:
- """Given an attribute access node (set or get), check if attribute
- access is legitimate.
- Call _check_first_attr with node before calling
- this method. Valid cases are:
- * self._attr in a method or cls._attr in a classmethod. Checked by
- _check_first_attr.
- * Klass._attr inside "Klass" class.
- * Klass2._attr inside "Klass" class when Klass2 is a base class of
- Klass.
- """
- attrname = node.attrname
- if (
- not is_attr_protected(attrname)
- or attrname in self.linter.config.exclude_protected
- ):
- return
- # Typing annotations in function definitions can include protected members
- if utils.is_node_in_type_annotation_context(node):
- return
- # Return if `attrname` is defined at the module-level or as a class attribute
- # and is listed in `exclude-protected`.
- inferred = safe_infer(node.expr)
- if (
- inferred
- and isinstance(inferred, (nodes.ClassDef, nodes.Module))
- and f"{inferred.name}.{attrname}" in self.linter.config.exclude_protected
- ):
- return
- klass = node_frame_class(node)
- if klass is None:
- # We are not in a class, no remaining valid case
- self.add_message("protected-access", node=node, args=attrname)
- return
- # In classes, check we are not getting a parent method
- # through the class object or through super
- # If the expression begins with a call to super, that's ok.
- match node.expr:
- case nodes.Call(func=nodes.Name(name="super")):
- return
- # If the expression begins with a call to type(self), that's ok.
- if self._is_type_self_call(node.expr):
- return
- # Check if we are inside the scope of a class or nested inner class
- inside_klass = True
- outer_klass = klass
- callee = node.expr.as_string()
- parents_callee = callee.split(".")
- for callee in reversed(parents_callee):
- if not (outer_klass and callee == outer_klass.name):
- inside_klass = False
- break
- # Move up one level within the nested classes
- outer_klass = get_outer_class(outer_klass)
- # We are in a class, one remaining valid cases, Klass._attr inside
- # Klass
- if not (inside_klass or callee in klass.basenames):
- # Detect property assignments in the body of the class.
- # This is acceptable:
- #
- # class A:
- # b = property(lambda: self._b)
- match node.parent.statement():
- case nodes.Assign(
- targets=[nodes.AssignName(name=name)]
- ) if _is_attribute_property(name, klass):
- return
- if (
- self._is_classmethod(node.frame())
- and self._is_inferred_instance(node.expr, klass)
- and self._is_class_or_instance_attribute(attrname, klass)
- ):
- return
- licit_protected_member = not attrname.startswith("__")
- if (
- not self.linter.config.check_protected_access_in_special_methods
- and licit_protected_member
- and self._is_called_inside_special_method(node)
- ):
- return
- self.add_message("protected-access", node=node, args=attrname)
- @staticmethod
- def _is_called_inside_special_method(node: nodes.NodeNG) -> bool:
- """Returns true if the node is located inside a special (aka dunder) method."""
- frame_name = node.frame().name
- return frame_name and frame_name in PYMETHODS
- def _is_type_self_call(self, expr: nodes.NodeNG) -> bool:
- match expr:
- case nodes.Call(func=nodes.Name(name="type"), args=[arg]):
- return self._is_mandatory_method_param(arg)
- return False
- @staticmethod
- def _is_classmethod(func: nodes.LocalsDictNodeNG) -> bool:
- """Check if the given *func* node is a class method."""
- return isinstance(func, nodes.FunctionDef) and (
- func.type == "classmethod" or func.name == "__class_getitem__"
- )
- @staticmethod
- def _is_inferred_instance(expr: nodes.NodeNG, klass: nodes.ClassDef) -> bool:
- """Check if the inferred value of the given *expr* is an instance of
- *klass*.
- """
- inferred = safe_infer(expr)
- if not isinstance(inferred, astroid.Instance):
- return False
- return inferred._proxied is klass
- @staticmethod
- def _is_class_or_instance_attribute(name: str, klass: nodes.ClassDef) -> bool:
- """Check if the given attribute *name* is a class or instance member of the
- given *klass*.
- Returns ``True`` if the name is a property in the given klass,
- ``False`` otherwise.
- """
- if utils.is_class_attr(name, klass):
- return True
- try:
- klass.instance_attr(name)
- return True
- except astroid.NotFoundError:
- return False
- def _check_accessed_members(
- self, node: nodes.ClassDef, accessed: dict[str, list[_AccessNodes]]
- ) -> None:
- """Check that accessed members are defined."""
- excs = ("AttributeError", "Exception", "BaseException")
- for attr, nodes_lst in accessed.items():
- try:
- # is it a class attribute ?
- node.local_attr(attr)
- # yes, stop here
- continue
- except astroid.NotFoundError:
- pass
- # is it an instance attribute of a parent class ?
- try:
- next(node.instance_attr_ancestors(attr))
- # yes, stop here
- continue
- except StopIteration:
- pass
- # is it an instance attribute ?
- try:
- defstmts = node.instance_attr(attr)
- except astroid.NotFoundError:
- pass
- else:
- # filter out augment assignment nodes
- defstmts = [stmt for stmt in defstmts if stmt not in nodes_lst]
- if not defstmts:
- # only augment assignment for this node, no-member should be
- # triggered by the typecheck checker
- continue
- # filter defstmts to only pick the first one when there are
- # several assignments in the same scope
- scope = defstmts[0].scope()
- defstmts = [
- stmt
- for i, stmt in enumerate(defstmts)
- if i == 0 or stmt.scope() is not scope
- ]
- # if there are still more than one, don't attempt to be smarter
- # than we can be
- if len(defstmts) == 1:
- defstmt = defstmts[0]
- # check that if the node is accessed in the same method as
- # it's defined, it's accessed after the initial assignment
- frame = defstmt.frame()
- lno = defstmt.fromlineno
- for _node in nodes_lst:
- if (
- _node.frame() is frame
- and _node.fromlineno < lno
- and not astroid.are_exclusive(
- _node.statement(), defstmt, excs
- )
- ):
- self.add_message(
- "access-member-before-definition",
- node=_node,
- args=(attr, lno),
- )
- def _check_first_arg_for_type(
- self, node: nodes.FunctionDef, metaclass: bool
- ) -> None:
- """Check the name of first argument, expect:.
- * 'self' for a regular method
- * 'cls' for a class method or a metaclass regular method (actually
- valid-classmethod-first-arg value)
- * 'mcs' for a metaclass class method (actually
- valid-metaclass-classmethod-first-arg)
- * not one of the above for a static method
- """
- # don't care about functions with unknown argument (builtins)
- if node.args.args is None:
- return
- if node.args.posonlyargs:
- first_arg = node.args.posonlyargs[0].name
- elif node.args.args:
- first_arg = node.argnames()[0]
- else:
- first_arg = None
- self._first_attrs.append(first_arg)
- first = self._first_attrs[-1]
- # static method
- if node.type == "staticmethod":
- if (
- first_arg == "self"
- or first_arg in self.linter.config.valid_classmethod_first_arg
- or first_arg in self.linter.config.valid_metaclass_classmethod_first_arg
- ):
- self.add_message("bad-staticmethod-argument", args=first, node=node)
- return
- self._first_attrs[-1] = None
- elif "builtins.staticmethod" in node.decoratornames():
- # Check if there is a decorator which is not named `staticmethod`
- # but is assigned to one.
- return
- # class / regular method with no args
- elif not (
- node.args.args
- or node.args.posonlyargs
- or node.args.vararg
- or node.args.kwarg
- ):
- self.add_message("no-method-argument", node=node, args=node.name)
- # metaclass
- elif metaclass:
- # metaclass __new__ or classmethod
- if node.type == "classmethod":
- self._check_first_arg_config(
- first,
- self.linter.config.valid_metaclass_classmethod_first_arg,
- node,
- "bad-mcs-classmethod-argument",
- node.name,
- )
- # metaclass regular method
- else:
- self._check_first_arg_config(
- first,
- self.linter.config.valid_classmethod_first_arg,
- node,
- "bad-mcs-method-argument",
- node.name,
- )
- # regular class with class method
- elif node.type == "classmethod" or node.name == "__class_getitem__":
- self._check_first_arg_config(
- first,
- self.linter.config.valid_classmethod_first_arg,
- node,
- "bad-classmethod-argument",
- node.name,
- )
- # regular class with regular method without self as argument
- elif first != "self":
- self.add_message("no-self-argument", node=node, args=node.name)
- def _check_first_arg_config(
- self,
- first: str | None,
- config: Sequence[str],
- node: nodes.FunctionDef,
- message: str,
- method_name: str,
- ) -> None:
- if first not in config:
- if len(config) == 1:
- valid = repr(config[0])
- else:
- valid = ", ".join(repr(v) for v in config[:-1])
- valid = f"{valid} or {config[-1]!r}"
- self.add_message(message, args=(method_name, valid), node=node)
- def _check_bases_classes(self, node: nodes.ClassDef) -> None:
- """Check that the given class node implements abstract methods from
- base classes.
- """
- def is_abstract(method: nodes.FunctionDef) -> bool:
- return method.is_abstract(pass_is_abstract=False) # type: ignore[no-any-return]
- # check if this class abstract
- if class_is_abstract(node):
- return
- methods = sorted(
- unimplemented_abstract_methods(node, is_abstract).items(),
- key=lambda item: item[0],
- )
- for name, method in methods:
- owner = method.parent.frame()
- if owner is node:
- continue
- # owner is not this class, it must be a parent class
- # check that the ancestor's method is not abstract
- if name in node.locals:
- # it is redefined as an attribute or with a descriptor
- continue
- self.add_message(
- "abstract-method",
- node=node,
- args=(name, owner.name, node.name),
- confidence=INFERENCE,
- )
- def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> None:
- """Check that the __init__ method call super or ancestors'__init__
- method (unless it is used for type hinting with `typing.overload`).
- """
- if not self.linter.is_message_enabled(
- "super-init-not-called"
- ) and not self.linter.is_message_enabled("non-parent-init-called"):
- return
- to_call = _ancestors_to_call(klass_node)
- not_called_yet = dict(to_call)
- parents_with_called_inits: set[bases.UnboundMethod] = set()
- for stmt in node.nodes_of_class(nodes.Call):
- expr = stmt.func
- if not (isinstance(expr, nodes.Attribute) and expr.attrname == "__init__"):
- continue
- # skip the test if using super
- match expr.expr:
- case nodes.Call(func=nodes.Name(name="super")):
- return
- try:
- for klass in expr.expr.infer():
- if isinstance(klass, util.UninferableBase):
- continue
- # The inferred klass can be super(), which was
- # assigned to a variable and the `__init__`
- # was called later.
- #
- # base = super()
- # base.__init__(...)
- match klass:
- case astroid.Instance(
- _proxied=nodes.ClassDef(name="super") as p
- ) if is_builtin_object(p):
- return
- case objects.Super():
- return
- try:
- method = not_called_yet.pop(klass)
- # Record that the class' init has been called
- parents_with_called_inits.add(node_frame_class(method))
- except KeyError:
- if klass not in klass_node.ancestors(recurs=False):
- self.add_message(
- "non-parent-init-called", node=expr, args=klass.name
- )
- except astroid.InferenceError:
- continue
- for klass, method in not_called_yet.items():
- # Check if the init of the class that defines this init has already
- # been called.
- if node_frame_class(method) in parents_with_called_inits:
- return
- if utils.is_protocol_class(klass):
- return
- if decorated_with(node, ["typing.overload"]):
- continue
- self.add_message(
- "super-init-not-called",
- args=klass.name,
- node=node,
- confidence=INFERENCE,
- )
- def _check_signature(
- self,
- method1: nodes.FunctionDef,
- refmethod: nodes.FunctionDef,
- cls: nodes.ClassDef,
- ) -> None:
- """Check that the signature of the two given methods match."""
- if not (
- isinstance(method1, nodes.FunctionDef)
- and isinstance(refmethod, nodes.FunctionDef)
- ):
- self.add_message(
- "method-check-failed", args=(method1, refmethod), node=method1
- )
- return
- instance = cls.instantiate_class()
- method1 = astroid.scoped_nodes.function_to_method(method1, instance)
- refmethod = astroid.scoped_nodes.function_to_method(refmethod, instance)
- # Don't care about functions with unknown argument (builtins).
- if method1.args.args is None or refmethod.args.args is None:
- return
- # Ignore private to class methods.
- if is_attr_private(method1.name):
- return
- # Ignore setters, they have an implicit extra argument,
- # which shouldn't be taken in consideration.
- if is_property_setter(method1):
- return
- arg_differ_output = _different_parameters(
- refmethod, method1, dummy_parameter_regex=self._dummy_rgx
- )
- class_type = "overriding"
- if len(arg_differ_output) > 0:
- for msg in arg_differ_output:
- if "Number" in msg:
- total_args_method1 = len(method1.args.args)
- if method1.args.vararg:
- total_args_method1 += 1
- if method1.args.kwarg:
- total_args_method1 += 1
- if method1.args.kwonlyargs:
- total_args_method1 += len(method1.args.kwonlyargs)
- total_args_refmethod = len(refmethod.args.args)
- if refmethod.args.vararg:
- total_args_refmethod += 1
- if refmethod.args.kwarg:
- total_args_refmethod += 1
- if refmethod.args.kwonlyargs:
- total_args_refmethod += len(refmethod.args.kwonlyargs)
- error_type = "arguments-differ"
- msg_args = (
- msg
- + f"was {total_args_refmethod} in '{refmethod.parent.frame().name}.{refmethod.name}' and "
- f"is now {total_args_method1} in",
- class_type,
- f"{method1.parent.frame().name}.{method1.name}",
- )
- elif "renamed" in msg:
- error_type = "arguments-renamed"
- msg_args = (
- msg,
- class_type,
- f"{method1.parent.frame().name}.{method1.name}",
- )
- else:
- error_type = "arguments-differ"
- msg_args = (
- msg,
- class_type,
- f"{method1.parent.frame().name}.{method1.name}",
- )
- self.add_message(error_type, args=msg_args, node=method1)
- elif (
- len(method1.args.defaults) < len(refmethod.args.defaults)
- and not method1.args.vararg
- ):
- class_type = "overridden"
- self.add_message(
- "signature-differs", args=(class_type, method1.name), node=method1
- )
- def _uses_mandatory_method_param(
- self, node: nodes.Attribute | nodes.Assign | nodes.AssignAttr
- ) -> bool:
- """Check that attribute lookup name use first attribute variable name.
- Name is `self` for method, `cls` for classmethod and `mcs` for metaclass.
- """
- return self._is_mandatory_method_param(node.expr)
- def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool:
- """Check if nodes.Name corresponds to first attribute variable name.
- Name is `self` for method, `cls` for classmethod and `mcs` for metaclass.
- Static methods return False.
- """
- if self._first_attrs:
- first_attr = self._first_attrs[-1]
- else:
- # It's possible the function was already unregistered.
- match closest_func := utils.get_node_first_ancestor_of_type(
- node, nodes.FunctionDef
- ):
- case nodes.FunctionDef(
- args=nodes.Arguments(args=[nodes.AssignName(name=first_attr), *_])
- ) if closest_func.is_bound():
- pass
- case _:
- return False
- # pylint: disable=possibly-used-before-assignment
- return isinstance(node, nodes.Name) and node.name == first_attr
- def _ancestors_to_call(
- klass_node: nodes.ClassDef, method_name: str = "__init__"
- ) -> dict[nodes.ClassDef, bases.UnboundMethod]:
- """Return a dictionary where keys are the list of base classes providing
- the queried method, and so that should/may be called from the method node.
- """
- to_call: dict[nodes.ClassDef, bases.UnboundMethod] = {}
- for base_node in klass_node.ancestors(recurs=False):
- try:
- init_node = next(base_node.igetattr(method_name))
- if not isinstance(init_node, astroid.UnboundMethod):
- continue
- if init_node.is_abstract():
- continue
- to_call[base_node] = init_node
- except astroid.InferenceError:
- continue
- return to_call
|