Skip to main content

Add your description here

Project description

sergey

A Python linter with opinionated rules about import style, naming, and code structure. Runs as a CLI tool or as an LSP server for editor integration.

The primary intent of Sergey is to enforce my personal stylistic rules upon agentic code. However, you may also find these useful in standard development. Simultaneously, it is a testing space for me for agentic coding.

Installation

uv add sergey

Usage

CLI

sergey check path/to/file.py     # check a single file
sergey check src/ tests/         # check directories (recursive)
sergey check .                   # check the whole project

Exits with code 0 if no violations are found, 1 if any are found.

LSP server

sergey serve

Communicates over stdio using the Language Server Protocol. Configure your editor to launch this command as a language server for Python files.

Rules

Imports

Rule Description
IMP001 from module import name is disallowed when name is not itself a submodule. Use import module and reference module.name at call sites. Typing modules, __future__, and collections.abc are exempt (see IMP002 and IMP004).
IMP002 import typing and import typing_extensions are disallowed. Use from typing import X and from typing_extensions import X to import names directly.
IMP003 Dotted plain imports (import os.path) are disallowed. Use from os import path instead. collections.abc is exempt (see IMP004).
IMP004 import collections.abc is disallowed. Use from collections.abc import X to import names directly.

The four rules together enforce a consistent import style: every name you use is either a bare module you imported at the top level, or a submodule you accessed via from package import submodule.

Naming

Rule Description
NAM001 Functions annotated -> bool must start with a predicate prefix: is_, has_, can_, should_, will_, did_, or was_. Dunder methods are exempt. Leading underscores on private helpers are ignored (_is_valid passes).
NAM002 Single-character variable names are disallowed in assignments, for-loops, comprehensions, with-statements, and walrus expressions. The conventional throwaway _ is exempt.
NAM003 Single-character function and method parameter names are disallowed. Covers positional-only, regular, and keyword-only parameters. _, *args, and **kwargs are exempt. Lambda parameters are not checked.

Documentation

Rule Description
DOC001 Functions that contain explicit raise statements must include a Raises section in their docstring. Bare re-raises (raise with no argument) are exempt. Raises inside nested functions or classes belong to those scopes and are not counted against the outer function. Functions with no docstring are not checked. Both Google style (Raises:) and NumPy style (Raises / ------) are accepted.

Pydantic

Rule Description
PDT001 Every BaseModel subclass must have model_config = ConfigDict(frozen=...) with frozen explicitly set. This forces a deliberate decision about mutability. Both frozen=True and frozen=False are accepted; omitting frozen or omitting model_config entirely is flagged.
PDT002 Frozen BaseModel subclasses (frozen=True) must not have fields annotated with mutable types such as list, dict, set, deque, etc. Use immutable alternatives (tuple, frozenset, …) instead. The check recurses into generic parameters and union syntax, so Optional[list[str]] and str | list[int] are both caught. ClassVar annotations are exempt.
PDT003 Fields in non-frozen BaseModel subclasses (frozen=False) must each declare their own Field(frozen=True) or Field(frozen=False). This forces a deliberate per-field immutability decision rather than silently inheriting the model-wide mutable default. The Field(frozen=...) may appear as the default value or inside an Annotated annotation. ClassVar fields and model_config are exempt.

Structure

Rule Description
STR002 Control-flow blocks nested deeper than 4 levels are flagged. Counted constructs: if/elif/else, for, while, with, try, match. elif branches count at the same depth as their leading if. Function, class, and lambda definitions reset the counter, so nested functions are judged independently.
STR003 try bodies containing more than 4 statements are flagged. Statements are counted recursively (an if with branches contributes 1 plus all contained statements). Only the try: body is counted — except and finally blocks are not subject to this rule. Nested functions and classes reset the count.
STR004 List and set literals inside functions that are never mutated and are not part of the function output (return/yield) should use immutable alternatives: tuple instead of [] and frozenset instead of {}. Only plain literals are checked; constructor calls and comprehensions are not covered.
STR005 Module-level constants (SCREAMING_SNAKE_CASE names) must carry a Final annotation (name: Final = ...) so static type checkers can enforce that they are never reassigned. Plain assignments and non-Final annotations are flagged. Dunder names (__all__, __version__, etc.) are exempt. Only direct module.body assignments are checked; constants inside functions, classes, or nested blocks are not.
STR006 Module-level constants (SCREAMING_SNAKE_CASE names) must not be assigned mutable list or set literals. Final prevents rebinding but not in-place mutation, so use tuple instead of [...] and frozenset instead of {...} to enforce immutability at the value level. Only plain literals at module scope are checked; dict literals, constructor calls, and comprehensions are not covered.

Suppression

Suppress a single line

x = some_function()  # sergey: noqa
x = some_function()  # sergey: noqa: NAM002
x = some_function()  # sergey: noqa: NAM002, IMP001

Suppress an entire file

Place this comment anywhere in the file (position does not matter):

# sergey: disable-file
# sergey: disable-file: IMP001
# sergey: disable-file: IMP001, IMP002

Development

uv run ruff check .        # lint
uv run ruff format .       # format
uv run ty check            # type check
uv run pytest              # run tests
uv run sergey check .      # run sergey on itself

Adding a rule

  1. Create or extend a module in sergey/rules/ with a class that subclasses base.Rule and implements check(tree, source) -> list[Diagnostic].
  2. Register the rule in sergey/rules/__init__.py by adding an instance to ALL_RULES.
  3. Add tests in tests/rules/.
  4. Update the Rules section in this README to document the new rule.

Rule IDs follow the pattern CAT### where CAT is a short category prefix (IMP, NAM, STR, …) and ### is a three-digit number.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sergey_lint-0.1.2.tar.gz (31.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sergey_lint-0.1.2-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

Details for the file sergey_lint-0.1.2.tar.gz.

File metadata

  • Download URL: sergey_lint-0.1.2.tar.gz
  • Upload date:
  • Size: 31.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sergey_lint-0.1.2.tar.gz
Algorithm Hash digest
SHA256 88455f8ddc9435fe1746f1d5ec68b176d138a9e7d7e764d7ac146b4d4dd40221
MD5 96c685ed46af154700dba7dfa33563ca
BLAKE2b-256 83dbb7856b4f2754b2e7882358d6c1d9df1fc5685133896e2a7a92646967818b

See more details on using hashes here.

File details

Details for the file sergey_lint-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: sergey_lint-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 29.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sergey_lint-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3e78c2392ef850f5604c4ab7aba8e3e07460d95331e881c6d2e9739c2e60b8b0
MD5 5f142ce3a5c0fe219d8fbed84cb3985b
BLAKE2b-256 f45a5265c792e57a37890a434abc02464f13bb7dd578b9c8e87869c7097e1710

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page