linting¶
buff lints and formats .by source. it is ruff, so it reads ruff's
configuration out of pyproject.toml and every rule ruff ships is available:
BY is basedpython's own rule prefix. most of those rules look for a python
spelling of something basedpython has syntax for:
| code | name | what it finds |
|---|---|---|
BY001 |
manual-none-coalesce |
a conditional expression that is a ?? |
BY002 |
manual-optional-chain |
a conditional expression that is a ?. |
BY003 |
manual-isinstance |
an isinstance call that is an is |
BY004 |
manual-super-call |
super() where super will do |
BY007 |
manual-any-annotation |
Any where dynamic will do |
BY009 |
manual-unpack-annotation |
Unpack[…] where * will do |
BY010 |
manual-typeof-annotation |
TypeOf[…] where typeof will do |
BY011 |
manual-re-export |
import name as name, which is export |
BY012 |
redundant-typing-import |
an import of a member already implicit |
BY017 |
unnecessary-stub-body |
a : ... body an empty declaration needs no more |
BY019 |
manual-sentinel |
a Sentinel(…) assignment, which is sentinel |
BY020 |
manual-cast-call |
a typing.cast call, which is the cast keyword |
BY021 |
manual-property |
a @property, which is a declaration with accessors |
BY022 |
manual-modifier |
a decorator that is a modifier keyword |
BY023 |
manual-tuple-annotation |
a tuple[…] annotation, which is a tuple type |
BY101 |
redundant-none-coalesce |
a ?? whose fallback cannot change the result |
every one of them is fixable. BY020's fix is the only one that is always
unsafe — the cast! it writes is checked where typing.cast
is a no-op, so the rewrite adds a way for the program to fail.
BY021 is the only one that sometimes has no fix to offer. an accessor body is
re-rendered when it is lowered and does not keep a comment, so a property with a
comment in it is reported and left for you to move. a tuple type is re-rendered
the same way, so BY023's fix is unsafe on an annotation with a comment in it.
they also compose with the upstream rules that produce their input. SIM108
turns an if / else block into a conditional expression, and BY001 takes it
the rest of the way, so one --fix run rewrites
into
what the linter does not check¶
the linter reads one file at a time and has no types. anything that takes a type
to decide is by check's job instead, and reporting it in both places would
mean two answers that can disagree. that split is why a once callback the
callee never calls is once-not-called in the checker and has no BY rule
here, and why t[0] is not reported as a
tuple member access — whether t is a tuple is a type
question
upstream rules on .by source¶
ruff's own rules are written for python source, and two things follow from that.
a rule that resolves a name through its import does not see a name basedpython
resolves for you. UP045 rewrites Optional[int] to int | None when
Optional was imported, and says nothing when it was left to
implicit typing — the same annotation, reported or not
depending on a line that basedpython does not need
a rule that suggests a replacement suggests the python one. SIM108 above is
the case where that composes; where it does not, the suggestion is still valid
.by, just not the shortest way to write it. E721 is the exception, and names
basedpython's operators instead, because python's advice there — reach for is
— spells a type test in .by rather than the
identity comparison it means in python
a rule that reads a construct as the python it resembles is a false positive, and each one is answered where the misreading is, not by silencing the rule:
orandandinside a type expression are union and intersection, so the boolean rules —SIM222,SIM223,RUF021— say nothing there- a bare string in a type position is
the literal type, not a forward reference, so
UP037does not offer to take its quotes off isis a type test unless its right-hand side is a literal, soF632reports only what really compares identity: the===and!==spellings, and anisagainst a literal- a match type's
casearms are types, soE701does not read the:in one as opening a suite - a destructuring binder always binds, so its captures are
reported the way the equivalent python unpacking's are —
B007for a loop,F841never - a type parameter is not a constant, however the one-letter convention spells
it, so
assert T == intis not aSIM300yoda condition
F821 is the older instance of the same thing: an unqualified builder inside a
trailing-lambda block resolves against the block's
implicit receiver, and the linter cannot see receiver
types, so it defers every unresolved name inside a block to by check. the
same deferral covers self and an
enum variant written bare