flake8-elegant-objects 1.1.1__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/.github/workflows/lint.yml +7 -0
- flake8_elegant_objects-2.0.0/.github/workflows/publish.yml +87 -0
- flake8_elegant_objects-2.0.0/.github/workflows/test.yml +92 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/CHANGELOG.md +81 -0
- flake8_elegant_objects-2.0.0/PKG-INFO +224 -0
- flake8_elegant_objects-2.0.0/README.md +193 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/__init__.py +7 -8
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/__main__.py +12 -6
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/base.py +259 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/links.py +70 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_constructor_code.py +81 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_er_name.py +222 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_getters_setters.py +90 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_implementation_inheritance.py +152 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_impure_tests.py +76 -32
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_mutable_objects/base.py +38 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/copy_on_write_checker.py +22 -18
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/core.py +102 -79
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_mutable_objects/deep_checker.py +72 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/pattern_detectors.py +35 -37
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/shared_state_checker.py +16 -7
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_null.py +93 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_open_classes.py +116 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_orm.py +23 -15
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_public_methods_without_contracts.py +179 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_static.py +68 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_type_discrimination.py +70 -0
- flake8_elegant_objects-2.0.0/flake8_elegant_objects/noqa.py +36 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/pyproject.toml +9 -4
- flake8_elegant_objects-2.0.0/tests/test_cli.py +137 -0
- flake8_elegant_objects-2.0.0/tests/test_flake8_plugin.py +53 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_integration.py +4 -1
- flake8_elegant_objects-2.0.0/tests/test_links.py +43 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_constructor_code.py +11 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_er_name.py +74 -25
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_getters_setters.py +48 -8
- flake8_elegant_objects-2.0.0/tests/test_no_implementation_inheritance.py +194 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_impure_tests.py +44 -1
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_mutable_objects.py +11 -13
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_null.py +23 -0
- flake8_elegant_objects-2.0.0/tests/test_no_open_classes.py +150 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_orm.py +34 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_public_methods_without_contracts.py +99 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_static.py +36 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_type_discrimination.py +38 -0
- flake8_elegant_objects-2.0.0/tests/test_noqa.py +70 -0
- flake8_elegant_objects-2.0.0/tests/test_packaging.py +28 -0
- flake8_elegant_objects-2.0.0/tests/test_self_check.py +63 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/uv.lock +45 -2
- flake8_elegant_objects-1.1.1/.claude/settings.local.json +0 -8
- flake8_elegant_objects-1.1.1/.github/workflows/publish.yml +0 -54
- flake8_elegant_objects-1.1.1/.github/workflows/test.yml +0 -44
- flake8_elegant_objects-1.1.1/CLAUDE.md +0 -96
- flake8_elegant_objects-1.1.1/PKG-INFO +0 -260
- flake8_elegant_objects-1.1.1/README.md +0 -230
- flake8_elegant_objects-1.1.1/docs/EO011.md +0 -144
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/base.py +0 -200
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_constructor_code.py +0 -59
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_er_name.py +0 -306
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_getters_setters.py +0 -57
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_implementation_inheritance.py +0 -75
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/base.py +0 -57
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/contract_checker.py +0 -76
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/deep_checker.py +0 -130
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/factory_checker.py +0 -91
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_null.py +0 -50
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_public_methods_without_contracts.py +0 -125
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_static.py +0 -31
- flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_type_discrimination.py +0 -34
- flake8_elegant_objects-1.1.1/tests/test_no_implementation_inheritance.py +0 -78
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/.gitignore +0 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/LICENSE +0 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/__init__.py +0 -0
- {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Trusted publishing: PyPI verifies this workflow through OIDC and issues a
|
|
4
|
+
# short-lived token, so no API token is stored anywhere. It requires a pending
|
|
5
|
+
# publisher configured once at
|
|
6
|
+
# https://pypi.org/manage/project/flake8-elegant-objects/settings/publishing/
|
|
7
|
+
# naming this repository, this workflow file and the pypi environment.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v4
|
|
27
|
+
with:
|
|
28
|
+
version: 'latest'
|
|
29
|
+
enable-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
run: uv python install 3.10
|
|
33
|
+
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: uv sync
|
|
36
|
+
|
|
37
|
+
- name: Run unit tests
|
|
38
|
+
run: uv run pytest tests/ -q
|
|
39
|
+
|
|
40
|
+
- name: Build package
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Check that the built version matches the tag
|
|
44
|
+
if: github.event_name == 'release'
|
|
45
|
+
run: |
|
|
46
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
47
|
+
built=$(ls dist/*.whl | sed -E 's/.*-([0-9]+\.[0-9]+\.[0-9]+)-py3.*/\1/')
|
|
48
|
+
if [ "$tag" != "$built" ]; then
|
|
49
|
+
echo "tag $tag does not match built version $built"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# The 1.1.1 release shipped a console script that raised ImportError.
|
|
54
|
+
# An editable install hides that, so the wheel is installed on its own
|
|
55
|
+
# and actually run before anything reaches PyPI.
|
|
56
|
+
- name: Run the wheel in a clean environment
|
|
57
|
+
run: |
|
|
58
|
+
uv venv /tmp/clean
|
|
59
|
+
uv pip install --python /tmp/clean/bin/python dist/*.whl flake8
|
|
60
|
+
printf 'from typing import final\n\n@final\nclass Money:\n def __init__(self, cents):\n self.cents = cents\n' > /tmp/clean_sample.py
|
|
61
|
+
/tmp/clean/bin/flake8-elegant-objects /tmp/clean_sample.py
|
|
62
|
+
/tmp/clean/bin/python -m flake8 --version | grep flake8-elegant-objects
|
|
63
|
+
|
|
64
|
+
- name: Upload distribution
|
|
65
|
+
uses: actions/upload-artifact@v4
|
|
66
|
+
with:
|
|
67
|
+
name: dist
|
|
68
|
+
path: dist/
|
|
69
|
+
|
|
70
|
+
publish:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
needs: build
|
|
73
|
+
environment:
|
|
74
|
+
name: pypi
|
|
75
|
+
url: https://pypi.org/p/flake8-elegant-objects
|
|
76
|
+
permissions:
|
|
77
|
+
id-token: write
|
|
78
|
+
|
|
79
|
+
steps:
|
|
80
|
+
- name: Download distribution
|
|
81
|
+
uses: actions/download-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
name: dist
|
|
84
|
+
path: dist/
|
|
85
|
+
|
|
86
|
+
- name: Publish to PyPI
|
|
87
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Build and Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout code
|
|
26
|
+
uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v4
|
|
30
|
+
with:
|
|
31
|
+
version: 'latest'
|
|
32
|
+
enable-cache: true
|
|
33
|
+
|
|
34
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
35
|
+
run: uv python install ${{ matrix.python-version }}
|
|
36
|
+
|
|
37
|
+
- name: Check that the lockfile matches pyproject.toml
|
|
38
|
+
run: uv lock --check
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: uv sync
|
|
42
|
+
|
|
43
|
+
- name: Run unit tests
|
|
44
|
+
run: uv run pytest tests/ -v
|
|
45
|
+
|
|
46
|
+
package:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
needs: test
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- name: Checkout code
|
|
52
|
+
uses: actions/checkout@v4
|
|
53
|
+
|
|
54
|
+
- name: Install uv
|
|
55
|
+
uses: astral-sh/setup-uv@v4
|
|
56
|
+
with:
|
|
57
|
+
version: 'latest'
|
|
58
|
+
enable-cache: true
|
|
59
|
+
|
|
60
|
+
- name: Set up Python
|
|
61
|
+
run: uv python install 3.10
|
|
62
|
+
|
|
63
|
+
- name: Build package
|
|
64
|
+
run: uv build
|
|
65
|
+
|
|
66
|
+
# The 1.1.1 release shipped a console script that raised ImportError.
|
|
67
|
+
# An editable install hides that class of fault, so the built wheel is
|
|
68
|
+
# installed into an environment of its own and actually run.
|
|
69
|
+
- name: Install the built wheel into a clean environment
|
|
70
|
+
run: |
|
|
71
|
+
uv venv /tmp/clean
|
|
72
|
+
uv pip install --python /tmp/clean/bin/python dist/*.whl flake8
|
|
73
|
+
|
|
74
|
+
- name: Run the console script from the wheel
|
|
75
|
+
run: |
|
|
76
|
+
printf 'from typing import final\n\n@final\nclass Money:\n def __init__(self, cents):\n self.cents = cents\n' > /tmp/clean_sample.py
|
|
77
|
+
/tmp/clean/bin/flake8-elegant-objects /tmp/clean_sample.py
|
|
78
|
+
|
|
79
|
+
- name: Run the plugin through flake8 from the wheel
|
|
80
|
+
run: |
|
|
81
|
+
printf 'class Parser:\n def parse(self, text):\n return None\n' > /tmp/dirty_sample.py
|
|
82
|
+
/tmp/clean/bin/python -m flake8 --version | grep flake8-elegant-objects
|
|
83
|
+
if /tmp/clean/bin/python -m flake8 --select=EO /tmp/dirty_sample.py; then
|
|
84
|
+
echo "the plugin reported nothing on code that violates EO001"
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
- name: Upload distribution
|
|
89
|
+
uses: actions/upload-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist/
|
|
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.0] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
Rules changed shape in ways that will report differently on code that passed
|
|
11
|
+
1.1.1. Two codes are gone, one stopped enforcing something the principles do
|
|
12
|
+
not ask for, and three started catching what they had been missing. Expect new
|
|
13
|
+
findings on an unchanged codebase.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- `EO022` (missing factory methods) and `EO024` (missing immutability enforcement).
|
|
18
|
+
Both prescribed a technique rather than forbidding one, neither appears in the
|
|
19
|
+
Elegant Objects principles, and both duplicated `EO016` on the same line.
|
|
20
|
+
- Procedural verb detection in `EO002`/`EO003`/`EO004`. Methods are nouns or verbs
|
|
21
|
+
separated into queries and commands, so verb names are not violations. These codes
|
|
22
|
+
now report -er names only, as `EO001` always did.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The `flake8-elegant-objects` console script raised `ImportError`: it pointed at
|
|
27
|
+
`flake8_elegant_objects:main`, while `main` lives in `__main__.py`.
|
|
28
|
+
- `EO006` no longer reports a constructor docstring as code.
|
|
29
|
+
- `EO005` now reports a bare `return` in a function that returns values elsewhere.
|
|
30
|
+
- `EO009` now detects decorators reached through a module, such as
|
|
31
|
+
`@abc.abstractstaticmethod`.
|
|
32
|
+
- `EO010` now detects `typing.cast`, `builtins.isinstance`, `issubclass`,
|
|
33
|
+
`match`/`case` class patterns, and reflection through `__class__`, `__bases__`,
|
|
34
|
+
`__mro__` and `__subclasses__`.
|
|
35
|
+
- `EO007` now detects `@x.setter` methods, which are setters regardless of name.
|
|
36
|
+
- `EO012` no longer reports a test docstring as a statement, and now requires the
|
|
37
|
+
single assertion to be the last statement of the method. The count was already
|
|
38
|
+
enforced; the position was not, so an assertion followed by anything permitted
|
|
39
|
+
passed. Its three faults now carry three distinct messages under the one code.
|
|
40
|
+
- `EO001`-`EO004` now match the -er and -or suffixes themselves, including plurals,
|
|
41
|
+
instead of a closed list of about seventy words. The principle says "readers,
|
|
42
|
+
parsers, controllers, sorters, and so on", and the old list missed Iterator,
|
|
43
|
+
Visitor, Simulator, Handlers and everything else outside it. Ordinary nouns are
|
|
44
|
+
allowed by their final word, so ImmutableUser and TaskCounter still pass.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `EO028`: a class that is not final invites implementation inheritance. Protocols,
|
|
49
|
+
abstract base classes and test suites are exempt, since they exist to be extended.
|
|
50
|
+
- `EO029`: a class holding more than four attributes has stopped being one object.
|
|
51
|
+
- `EO009` now covers module level functions, which are the Python static method.
|
|
52
|
+
- `EO007` now covers `@property` accessors whose body is a single `return self._x`.
|
|
53
|
+
- `EO011` now covers `async` methods, and exempts test methods, which implement
|
|
54
|
+
no contract.
|
|
55
|
+
- Tests for the standalone CLI and for plugin registration through flake8 itself,
|
|
56
|
+
neither of which was covered before.
|
|
57
|
+
- `flake8` as a development dependency.
|
|
58
|
+
- A self check: the plugin runs over its own source and fails when any violation
|
|
59
|
+
count grows or a new code starts firing. The counts are debt, not a target.
|
|
60
|
+
- A check that the version flake8 reports matches the one the distribution
|
|
61
|
+
declares. The two were written out independently in `pyproject.toml` and
|
|
62
|
+
`__init__.py`.
|
|
63
|
+
- A coverage floor of 90 percent, which the README badge had been claiming
|
|
64
|
+
without anything enforcing it.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- The plugin obeys every rule it enforces. What began as 300 violations against
|
|
69
|
+
its own source is 0, with 23 suppressions that each name a reason: entry point
|
|
70
|
+
signatures flake8 and pyproject dictate, one isinstance inside `Instance`, one
|
|
71
|
+
`None` for the absence of an enclosing class, `Generic` for TypeGuard narrowing,
|
|
72
|
+
and helper methods whose contracts would mean a Protocol apiece. A test fixes
|
|
73
|
+
that count, so a new suppression has to be taken deliberately.
|
|
74
|
+
- Type discrimination lives in one object. `Instance(kind).covers(node)` replaced
|
|
75
|
+
148 direct `isinstance` calls across nineteen files; its answer is a `TypeGuard`,
|
|
76
|
+
so callers keep the narrowing they had.
|
|
77
|
+
- Error messages moved from the `ErrorCodes` class to module constants, and the
|
|
78
|
+
parent of a node is a real map rather than an attribute stitched onto borrowed
|
|
79
|
+
ast objects.
|
|
80
|
+
|
|
81
|
+
- CI installs the built wheel into a clean environment and runs the console
|
|
82
|
+
script and `flake8 --select=EO` from it. Editable installs hide the fault
|
|
83
|
+
that shipped in 1.1.1.
|
|
84
|
+
- CI checks the lockfile against `pyproject.toml`, tests Python 3.14, caches uv,
|
|
85
|
+
cancels superseded runs and restricts token permissions to reading contents.
|
|
86
|
+
- Linting, formatting and type checking no longer run in both workflows. They
|
|
87
|
+
ran four times per push, once per Python version, and now run once.
|
|
88
|
+
|
|
8
89
|
## [1.1.1] - 2024-06-30
|
|
9
90
|
|
|
10
91
|
### Minor release
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flake8-elegant-objects
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Flake8 plugin enforcing Elegant Objects principles: no -er naming, no null, no getters/setters, immutable objects
|
|
5
|
+
Project-URL: Homepage, https://github.com/AntonProkopyev/flake8-elegant-objects
|
|
6
|
+
Project-URL: Repository, https://github.com/AntonProkopyev/flake8-elegant-objects.git
|
|
7
|
+
Project-URL: Issues, https://github.com/AntonProkopyev/flake8-elegant-objects/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/AntonProkopyev/flake8-elegant-objects#readme
|
|
9
|
+
Author: Anton Prokopyev
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: code-quality,elegant-objects,flake8,linting,object-oriented,python,static-analysis
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Framework :: Flake8
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# flake8-elegant-objects
|
|
33
|
+
|
|
34
|
+
[](https://github.com/AntonProkopyev/flake8-elegant-objects)
|
|
35
|
+
[](https://github.com/AntonProkopyev/flake8-elegant-objects)
|
|
36
|
+
[](https://python.org)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://www.elegantobjects.org)
|
|
39
|
+
|
|
40
|
+
A flake8 plugin that checks Python against the
|
|
41
|
+
[Elegant Objects](https://www.elegantobjects.org/) principles: no null, no code
|
|
42
|
+
in constructors, no getters and setters, no mutable objects, no -er names, no
|
|
43
|
+
static methods, no type discrimination, no public methods without a contract,
|
|
44
|
+
no statements in tests beyond the assertion, no ORM, no implementation
|
|
45
|
+
inheritance.
|
|
46
|
+
|
|
47
|
+
Every message ends with a link to the article the rule comes from, so a code
|
|
48
|
+
you have not met before explains itself.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install flake8-elegant-objects
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Run
|
|
57
|
+
|
|
58
|
+
Through flake8, which is how most projects will want it:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
flake8 --select=EO your_package/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The plugin registers itself on install; `--select=EO` narrows the run to its
|
|
65
|
+
codes. Individual codes work too, and so does `# noqa: EO001`.
|
|
66
|
+
|
|
67
|
+
Standalone, when you want it without flake8:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m flake8_elegant_objects your_package/*.py
|
|
71
|
+
flake8-elegant-objects --show-source your_package/*.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Both forms honour `noqa` comments and report the same violations.
|
|
75
|
+
|
|
76
|
+
## Codes
|
|
77
|
+
|
|
78
|
+
**Naming.** The principle names "readers, parsers, controllers, sorters, and so
|
|
79
|
+
on", so the -er and -or suffixes are matched rather than a fixed list of words.
|
|
80
|
+
Ordinary nouns are allowed by their final word: `ImmutableUser` and
|
|
81
|
+
`TaskCounter` pass, `Formatter` and `Recorder` do not.
|
|
82
|
+
|
|
83
|
+
| | |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `EO001` | class name ends in -er |
|
|
86
|
+
| `EO002` | method name ends in -er |
|
|
87
|
+
| `EO003` | variable name ends in -er |
|
|
88
|
+
| `EO004` | function name ends in -er |
|
|
89
|
+
|
|
90
|
+
Verb names are not violations. A method is a noun that builds or a verb that
|
|
91
|
+
commands, and the principle objects to actors, not to verbs.
|
|
92
|
+
|
|
93
|
+
**Objects.**
|
|
94
|
+
|
|
95
|
+
| | |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `EO005` | null used, including a bare `return` beside returns that carry a value |
|
|
98
|
+
| `EO006` | a constructor doing anything beyond binding its parameters |
|
|
99
|
+
| `EO007` | a getter or setter, whether spelled `get_name` or `@property` |
|
|
100
|
+
| `EO009` | a static method, a class method, or a module level function |
|
|
101
|
+
| `EO010` | `isinstance`, `type`, `cast`, reflection, or a `match` on classes |
|
|
102
|
+
| `EO011` | a public method with no Protocol or ABC behind it |
|
|
103
|
+
| `EO012` | a test holding anything but one assertion, closing with it |
|
|
104
|
+
| `EO013` | an ORM or ActiveRecord call |
|
|
105
|
+
| `EO014` | inheritance from something that is not a contract |
|
|
106
|
+
| `EO028` | a class left open, which invites implementation inheritance |
|
|
107
|
+
| `EO029` | a class holding more than four attributes |
|
|
108
|
+
|
|
109
|
+
**Mutability.** One principle, several shapes.
|
|
110
|
+
|
|
111
|
+
| | |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `EO008` | a dataclass that is not frozen |
|
|
114
|
+
| `EO015` | a mutable class attribute |
|
|
115
|
+
| `EO016` | a mutable instance attribute |
|
|
116
|
+
| `EO017` | an instance attribute assigned outside the constructor |
|
|
117
|
+
| `EO018` | an augmented assignment to an attribute |
|
|
118
|
+
| `EO019` | a call that mutates in place, such as `append` |
|
|
119
|
+
| `EO020` | a subscript assignment to an attribute |
|
|
120
|
+
| `EO021` | a mutation reached through a chain |
|
|
121
|
+
| `EO023` | a mutable default argument |
|
|
122
|
+
| `EO025` | a method that mutates where it could return a new instance |
|
|
123
|
+
| `EO026` | internal mutable state handed out |
|
|
124
|
+
| `EO027` | a mutable argument stored without a copy |
|
|
125
|
+
|
|
126
|
+
## Where this is stricter or looser than the principles
|
|
127
|
+
|
|
128
|
+
`EO002`, `EO003` and `EO004` extend a rule about class names to methods,
|
|
129
|
+
variables and functions. `EO028` and `EO029` are not on elegantobjects.org at
|
|
130
|
+
all; they come from the
|
|
131
|
+
[rules yegor256 gives his own agents](https://github.com/yegor256/prompt).
|
|
132
|
+
|
|
133
|
+
`EO011` and `EO014` do not judge a base class they cannot see. A Protocol
|
|
134
|
+
declared in one module and implemented in another is ordinary, and a plugin
|
|
135
|
+
reading one file at a time has no evidence about an imported name. A base
|
|
136
|
+
defined in the file and not a contract is still reported.
|
|
137
|
+
|
|
138
|
+
`EO012` allows a bare `assert` and `self.assertEqual`, because Python has no
|
|
139
|
+
Hamcrest and `assertThat` would be a foreign requirement.
|
|
140
|
+
|
|
141
|
+
Test methods are exempt from `EO011`: they implement no contract.
|
|
142
|
+
|
|
143
|
+
## The plugin on itself
|
|
144
|
+
|
|
145
|
+
It passes. Run it over its own source and nothing is reported that is not
|
|
146
|
+
marked, and each mark says why:
|
|
147
|
+
|
|
148
|
+
- `EO009` and `EO010` where flake8 and `pyproject.toml` dictate a signature
|
|
149
|
+
- `EO010` once inside `Instance`, the object every type check goes through,
|
|
150
|
+
because a linter reading syntax trees cannot ask a node what it is any other
|
|
151
|
+
way
|
|
152
|
+
- `EO005` once, for the absence of an enclosing class, where a null object
|
|
153
|
+
would answer with lies rather than with nothing
|
|
154
|
+
- `EO011` on helper methods whose contracts would cost a Protocol apiece
|
|
155
|
+
- `EO014` on `Generic`, which is what keeps type narrowing alive
|
|
156
|
+
|
|
157
|
+
`tests/test_self_check.py` fixes that count, so a new suppression has to be
|
|
158
|
+
argued for rather than added in passing.
|
|
159
|
+
|
|
160
|
+
## Configure
|
|
161
|
+
|
|
162
|
+
```ini
|
|
163
|
+
[flake8]
|
|
164
|
+
select = E,W,F,EO
|
|
165
|
+
per-file-ignores =
|
|
166
|
+
tests/*:EO011,EO012
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If you use ruff alongside, tell it these codes belong to someone else, or it
|
|
170
|
+
will strip the `noqa` comments that name them:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
[tool.ruff.lint]
|
|
174
|
+
external = ["EO"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Develop
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv sync
|
|
181
|
+
uv run pytest tests/
|
|
182
|
+
uv run ruff check . && uv run ruff format --check .
|
|
183
|
+
uv run mypy .
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Layout
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
flake8_elegant_objects/
|
|
190
|
+
├── __init__.py # the flake8 plugin
|
|
191
|
+
├── __main__.py # the standalone command
|
|
192
|
+
├── base.py # Instance, Source, Violation, messages
|
|
193
|
+
├── links.py # the article behind each code
|
|
194
|
+
├── noqa.py # suppression comments outside flake8
|
|
195
|
+
├── no_constructor_code.py # EO006
|
|
196
|
+
├── no_er_name.py # EO001-EO004
|
|
197
|
+
├── no_getters_setters.py # EO007
|
|
198
|
+
├── no_implementation_inheritance.py # EO014
|
|
199
|
+
├── no_impure_tests.py # EO012
|
|
200
|
+
├── no_null.py # EO005
|
|
201
|
+
├── no_open_classes.py # EO028, EO029
|
|
202
|
+
├── no_orm.py # EO013
|
|
203
|
+
├── no_public_methods_without_contracts.py # EO011
|
|
204
|
+
├── no_static.py # EO009
|
|
205
|
+
├── no_type_discrimination.py # EO010
|
|
206
|
+
└── no_mutable_objects/ # EO008, EO015-EO027
|
|
207
|
+
├── base.py # what counts as mutable
|
|
208
|
+
├── core.py # the orchestrator
|
|
209
|
+
├── copy_on_write_checker.py # EO025
|
|
210
|
+
├── deep_checker.py # mutations across a class
|
|
211
|
+
├── pattern_detectors.py # EO026, EO027
|
|
212
|
+
└── shared_state_checker.py # shared mutable state
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Reading
|
|
216
|
+
|
|
217
|
+
- [Elegant Objects](https://www.elegantobjects.org/), the principles themselves
|
|
218
|
+
- [Object Thinking](http://amzn.to/2BVeiNl) by David West
|
|
219
|
+
- [Elegant Objects](http://www.yegor256.com/elegant-objects.html) vol. 1 and 2
|
|
220
|
+
by Yegor Bugayenko
|
|
221
|
+
|
|
222
|
+
## Licence
|
|
223
|
+
|
|
224
|
+
MIT. See [LICENSE](LICENSE).
|