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.
Files changed (74) hide show
  1. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/.github/workflows/lint.yml +7 -0
  2. flake8_elegant_objects-2.0.0/.github/workflows/publish.yml +87 -0
  3. flake8_elegant_objects-2.0.0/.github/workflows/test.yml +92 -0
  4. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/CHANGELOG.md +81 -0
  5. flake8_elegant_objects-2.0.0/PKG-INFO +224 -0
  6. flake8_elegant_objects-2.0.0/README.md +193 -0
  7. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/__init__.py +7 -8
  8. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/__main__.py +12 -6
  9. flake8_elegant_objects-2.0.0/flake8_elegant_objects/base.py +259 -0
  10. flake8_elegant_objects-2.0.0/flake8_elegant_objects/links.py +70 -0
  11. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_constructor_code.py +81 -0
  12. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_er_name.py +222 -0
  13. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_getters_setters.py +90 -0
  14. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_implementation_inheritance.py +152 -0
  15. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_impure_tests.py +76 -32
  16. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_mutable_objects/base.py +38 -0
  17. {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
  18. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/core.py +102 -79
  19. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_mutable_objects/deep_checker.py +72 -0
  20. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/pattern_detectors.py +35 -37
  21. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/shared_state_checker.py +16 -7
  22. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_null.py +93 -0
  23. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_open_classes.py +116 -0
  24. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_orm.py +23 -15
  25. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_public_methods_without_contracts.py +179 -0
  26. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_static.py +68 -0
  27. flake8_elegant_objects-2.0.0/flake8_elegant_objects/no_type_discrimination.py +70 -0
  28. flake8_elegant_objects-2.0.0/flake8_elegant_objects/noqa.py +36 -0
  29. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/pyproject.toml +9 -4
  30. flake8_elegant_objects-2.0.0/tests/test_cli.py +137 -0
  31. flake8_elegant_objects-2.0.0/tests/test_flake8_plugin.py +53 -0
  32. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_integration.py +4 -1
  33. flake8_elegant_objects-2.0.0/tests/test_links.py +43 -0
  34. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_constructor_code.py +11 -0
  35. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_er_name.py +74 -25
  36. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_getters_setters.py +48 -8
  37. flake8_elegant_objects-2.0.0/tests/test_no_implementation_inheritance.py +194 -0
  38. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_impure_tests.py +44 -1
  39. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_mutable_objects.py +11 -13
  40. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_null.py +23 -0
  41. flake8_elegant_objects-2.0.0/tests/test_no_open_classes.py +150 -0
  42. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_orm.py +34 -0
  43. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_public_methods_without_contracts.py +99 -0
  44. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_static.py +36 -0
  45. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/test_no_type_discrimination.py +38 -0
  46. flake8_elegant_objects-2.0.0/tests/test_noqa.py +70 -0
  47. flake8_elegant_objects-2.0.0/tests/test_packaging.py +28 -0
  48. flake8_elegant_objects-2.0.0/tests/test_self_check.py +63 -0
  49. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/uv.lock +45 -2
  50. flake8_elegant_objects-1.1.1/.claude/settings.local.json +0 -8
  51. flake8_elegant_objects-1.1.1/.github/workflows/publish.yml +0 -54
  52. flake8_elegant_objects-1.1.1/.github/workflows/test.yml +0 -44
  53. flake8_elegant_objects-1.1.1/CLAUDE.md +0 -96
  54. flake8_elegant_objects-1.1.1/PKG-INFO +0 -260
  55. flake8_elegant_objects-1.1.1/README.md +0 -230
  56. flake8_elegant_objects-1.1.1/docs/EO011.md +0 -144
  57. flake8_elegant_objects-1.1.1/flake8_elegant_objects/base.py +0 -200
  58. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_constructor_code.py +0 -59
  59. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_er_name.py +0 -306
  60. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_getters_setters.py +0 -57
  61. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_implementation_inheritance.py +0 -75
  62. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/base.py +0 -57
  63. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/contract_checker.py +0 -76
  64. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/deep_checker.py +0 -130
  65. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_mutable_objects/factory_checker.py +0 -91
  66. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_null.py +0 -50
  67. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_public_methods_without_contracts.py +0 -125
  68. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_static.py +0 -31
  69. flake8_elegant_objects-1.1.1/flake8_elegant_objects/no_type_discrimination.py +0 -34
  70. flake8_elegant_objects-1.1.1/tests/test_no_implementation_inheritance.py +0 -78
  71. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/.gitignore +0 -0
  72. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/LICENSE +0 -0
  73. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/flake8_elegant_objects/no_mutable_objects/__init__.py +0 -0
  74. {flake8_elegant_objects-1.1.1 → flake8_elegant_objects-2.0.0}/tests/__init__.py +0 -0
@@ -6,6 +6,13 @@ on:
6
6
  pull_request:
7
7
  branches: [master, main]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
9
16
  jobs:
10
17
  lint:
11
18
  runs-on: ubuntu-latest
@@ -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
+ [![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](https://github.com/AntonProkopyev/flake8-elegant-objects)
35
+ [![Coverage](https://img.shields.io/badge/coverage-94%25-brightgreen.svg)](https://github.com/AntonProkopyev/flake8-elegant-objects)
36
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
37
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
38
+ [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](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).