@@ -254,6 +254,7 @@ class ClassNameCheck(BaseASTCheck):
254254 Classes for internal use have a leading underscore in addition.
255255 """
256256 N801 = "class name '{name}' should use CapWords convention"
257+ N808 = "type variable name '{name}' should use CapWords convention and an optional suffix '_co' or '_contra'"
257258 N818 = "exception name '{name}' should be named with an Error suffix"
258259
259260 @classmethod
@@ -275,6 +276,40 @@ def superclass_names(cls, name, parents: Iterable, _names=None):
275276 names .update (cls .superclass_names (base .id , parents , names ))
276277 return names
277278
279+ def visit_module (self , node , parents : Iterable , ignore = None ):
280+ for body in node .body :
281+ try :
282+ if len (body .targets ) != 1 :
283+ continue
284+ name = body .targets [0 ].id
285+ func_name = body .value .func .id
286+ args = [a .value for a in body .value .args ]
287+ keywords = {kw .arg : kw .value .value for kw in body .value .keywords }
288+ except AttributeError :
289+ continue
290+
291+ if func_name != "TypeVar" or _ignored (name , ignore ):
292+ continue
293+
294+ if len (args ) == 0 or args [0 ] != name :
295+ yield self .err (body , 'N808' , name = name )
296+
297+ if not name [:1 ].isupper ():
298+ yield self .err (body , 'N808' , name = name )
299+
300+ parts = name .split ('_' )
301+ if len (parts ) > 2 :
302+ yield self .err (body , 'N808' , name = name )
303+
304+ suffix = parts [- 1 ] if len (parts ) > 1 else ''
305+ if suffix and suffix != 'co' and suffix != 'contra' :
306+ yield self .err (body , 'N808' , name = name )
307+ elif keywords .get ('covariant' ) and suffix != 'co' :
308+ yield self .err (body , 'N808' , name = name )
309+ elif keywords .get ('contravariant' ) and suffix != 'contra' :
310+ yield self .err (body , 'N808' , name = name )
311+
312+
278313 def visit_classdef (self , node , parents : Iterable , ignore = None ):
279314 name = node .name
280315 if _ignored (name , ignore ):
0 commit comments