encoding-claim-audit 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,2 @@
1
+ """Audit documented Python text encoding claims."""
2
+ __version__ = '0.1.0'
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,132 @@
1
+ """Offline documentation and AST encoding audit."""
2
+ import ast
3
+ import re
4
+ from pathlib import Path
5
+
6
+ RULE = 'documented-encoding-claim-false'
7
+ TEST_RULE = RULE + '-test-only'
8
+ FAMILIES = {'UTF-8': r'\butf[-_]?8\b', 'Unicode': r'\bunicode\b'}
9
+
10
+
11
+ def claims_in(text):
12
+ """Return claim families in unfenced, non-negated prose."""
13
+ prose = []
14
+ fence = None
15
+ for line in text.splitlines():
16
+ match = re.match(r'^\s{0,3}(`{3,}|~{3,})', line)
17
+ if match:
18
+ marker = match.group(1)
19
+ if fence is None:
20
+ fence = marker
21
+ elif marker[0] == fence[0] and len(marker) >= len(fence):
22
+ fence = None
23
+ continue
24
+ if fence is None:
25
+ prose.append(line)
26
+ result = set()
27
+ for sentence in re.split(r'[.!?\n]+', '\n'.join(prose)):
28
+ if re.search(r"\b(no|not|never|without)\b|\b(?:doesn’t|doesn't|cannot|can't)\b", sentence, re.I):
29
+ continue
30
+ for family, pattern in FAMILIES.items():
31
+ if re.search(pattern, sentence, re.I):
32
+ result.add(family)
33
+ return result
34
+
35
+
36
+ def _name(node):
37
+ if isinstance(node, ast.Name):
38
+ return node.id
39
+ if isinstance(node, ast.Attribute):
40
+ return _name(node.value) + '.' + node.attr
41
+ return ''
42
+
43
+
44
+ def inspect_source(text, path):
45
+ """Return evidence and number of statically recognized text sites."""
46
+ tree = ast.parse(text, filename=path)
47
+ aliases = {}
48
+ for node in ast.walk(tree):
49
+ if isinstance(node, ast.Import):
50
+ for item in node.names:
51
+ aliases[item.asname or item.name] = item.name
52
+ elif isinstance(node, ast.ImportFrom) and node.module:
53
+ for item in node.names:
54
+ aliases[item.asname or item.name] = node.module + '.' + item.name
55
+ evidence = []
56
+ sites = 0
57
+ for node in ast.walk(tree):
58
+ if not isinstance(node, ast.Call):
59
+ continue
60
+ name = _name(node.func)
61
+ first, _, rest = name.partition('.')
62
+ name = aliases.get(first, first) + ('.' + rest if rest else '')
63
+ keywords = {k.arg: k.value for k in node.keywords}
64
+ encoding = keywords.get('encoding')
65
+ if name in ('open', 'io.open', 'builtins.open', 'codecs.open'):
66
+ mode = keywords.get('mode', node.args[1] if len(node.args) > 1 else ast.Constant('r'))
67
+ if not isinstance(mode, ast.Constant) or not isinstance(mode.value, str):
68
+ continue
69
+ if 'b' in mode.value:
70
+ continue
71
+ position = 2 if name == 'codecs.open' else 3
72
+ if encoding is None and len(node.args) > position:
73
+ encoding = node.args[position]
74
+ elif name == 'io.TextIOWrapper' or (isinstance(node.func, ast.Attribute) and node.func.attr in ('read_text', 'write_text')):
75
+ pass
76
+ else:
77
+ continue
78
+ sites += 1
79
+ if encoding is None:
80
+ kind = 'missing-encoding'
81
+ elif isinstance(encoding, ast.Constant):
82
+ if isinstance(encoding.value, str) and encoding.value.lower() in ('utf-8', 'utf8', 'utf_8'):
83
+ continue
84
+ kind = 'wrong-encoding'
85
+ else:
86
+ continue
87
+ evidence.append({'path': path, 'line': node.lineno, 'kind': kind})
88
+ return evidence, sites
89
+
90
+
91
+ def audit(root, ignore=()):
92
+ """Audit a directory without importing or executing its source."""
93
+ root = Path(root)
94
+ if not root.is_dir():
95
+ raise ValueError('input path must be a directory')
96
+ claims = set()
97
+ docs = [root / 'README.md']
98
+ if (root / 'docs').is_dir() and not (root / 'docs').is_symlink():
99
+ docs.extend((root / 'docs').rglob('*.md'))
100
+ def safe(path):
101
+ return not any(p.is_symlink() for p in [path] + list(path.parents) if p != root.parent)
102
+ for path in docs:
103
+ if path.is_file() and safe(path):
104
+ claims.update(claims_in(path.read_text(encoding='utf-8')))
105
+ if not claims:
106
+ return {'findings': [], 'verified_claims': [], 'summary': {'claims': 0, 'findings': 0, 'python_files': 0}}
107
+ production, testonly = [], []
108
+ sources = sites = 0
109
+ for path in sorted(root.rglob('*.py')):
110
+ relative = path.relative_to(root)
111
+ if not safe(path) or 'examples' in relative.parts:
112
+ continue
113
+ sources += 1
114
+ evidence, count = inspect_source(path.read_text(encoding='utf-8-sig'), relative.as_posix())
115
+ is_test = bool(set(relative.parts[:-1]) & {'tests', 'test', 'fixtures'})
116
+ (testonly if is_test else production).extend(evidence)
117
+ if not is_test:
118
+ sites += count
119
+ findings, verified = [], []
120
+ for family in sorted(claims):
121
+ if production or not sources:
122
+ rule, severity, evidence = RULE, 'HIGH', production
123
+ note = 'Production text I/O contradicts the claim.' if sources else 'No Python source files found.'
124
+ elif testonly:
125
+ rule, severity, evidence = TEST_RULE, 'MEDIUM', testonly
126
+ note = 'Encoding evidence occurs only in tests or fixtures.'
127
+ else:
128
+ verified.append({'family': family, 'note': 'All recognized production text I/O carries UTF-8 encoding or an unknown encoding.' if sites else 'No production text I/O; claim is moot, not falsified.'})
129
+ continue
130
+ if rule not in ignore:
131
+ findings.append({'rule': rule, 'severity': severity, 'family': family, 'message': note, 'evidence': evidence})
132
+ return {'findings': findings, 'verified_claims': verified, 'summary': {'claims': len(claims), 'findings': len(findings), 'python_files': sources}}
@@ -0,0 +1,46 @@
1
+ """Command line interface."""
2
+ import argparse
3
+ import json
4
+ import sys
5
+ from . import __version__
6
+ from .audit import RULE, TEST_RULE, audit
7
+
8
+
9
+ class Parser(argparse.ArgumentParser):
10
+ def error(self, message):
11
+ self.print_usage(sys.stderr)
12
+ self.exit(1, '%s: error: %s\n' % (self.prog, message))
13
+
14
+
15
+ def main(argv=None):
16
+ parser = Parser(prog='encoding-claim-audit')
17
+ parser.add_argument('path', nargs='?', default='.')
18
+ parser.add_argument('--json', action='store_true')
19
+ parser.add_argument('--fail-on', choices=('HIGH', 'MEDIUM'), default='MEDIUM')
20
+ parser.add_argument('--ignore', default='', metavar='RULE,...')
21
+ parser.add_argument('--verbose', action='store_true')
22
+ parser.add_argument('--version', action='version', version=__version__)
23
+ args = parser.parse_args(argv)
24
+ ignored = set(filter(None, args.ignore.split(',')))
25
+ if ignored - {RULE, TEST_RULE}:
26
+ parser.error('unknown ignored rule: ' + ', '.join(sorted(ignored - {RULE, TEST_RULE})))
27
+ try:
28
+ result = audit(args.path, ignored)
29
+ except (OSError, ValueError, SyntaxError) as exc:
30
+ print('encoding-claim-audit: ' + str(exc), file=sys.stderr)
31
+ return 1
32
+ failed = any(f['severity'] == 'HIGH' or args.fail_on == 'MEDIUM' for f in result['findings'])
33
+ result['summary']['status'] = 'FAIL' if failed else 'PASS'
34
+ if args.json:
35
+ print(json.dumps(result, indent=2))
36
+ else:
37
+ print(result['summary']['status'])
38
+ for finding in result['findings']:
39
+ print('{severity} {rule} [{family}]: {message}'.format(**finding))
40
+ for item in finding['evidence']:
41
+ print(' {path}:{line}: {kind}'.format(**item))
42
+ if args.verbose:
43
+ for claim in result['verified_claims']:
44
+ print('{family}: {note}'.format(**claim))
45
+ print('Python files: ' + str(result['summary']['python_files']))
46
+ return 2 if failed else 0
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: encoding-claim-audit
3
+ Version: 0.1.0
4
+ Summary: Offline audit of documented text encoding claims against Python source
5
+ Author: encoding-claim-audit contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # encoding-claim-audit
13
+
14
+ Version 0.1.0. Offline static audit of documented UTF-8 and Unicode claims
15
+ against Python text file I/O. Python >=3.9; no runtime dependencies.
16
+ Audited code is never executed and no network requests are made.
17
+
18
+ ## Usage
19
+
20
+ ```sh
21
+ python3 -m pip install .
22
+ encoding-claim-audit [path]
23
+ encoding-claim-audit ./project --json --fail-on HIGH
24
+ encoding-claim-audit ./project --ignore documented-encoding-claim-false-test-only
25
+ encoding-claim-audit --version
26
+ ```
27
+
28
+ Path defaults to `.`. `--verbose` prints verified claim notes and source counts.
29
+ `--json` emits findings, verified_claims, and summary. Each finding has a family,
30
+ rule, severity, message, and evidence items with path, line, and kind.
31
+ Exit codes: 0 PASS at the selected threshold, 2 FAIL, 1 usage/input error.
32
+ The default threshold is MEDIUM. `--fail-on HIGH` still prints MEDIUM findings.
33
+ Unknown ignore rules are input errors. `--ignore RULE,...` accepts:
34
+
35
+ - `documented-encoding-claim-false`: HIGH for production evidence or no source.
36
+ - `documented-encoding-claim-false-test-only`: MEDIUM for test-only evidence.
37
+
38
+ Each family produces at most one finding. Production evidence takes precedence.
39
+ Ignored findings are omitted, not relabeled as verified.
40
+
41
+ ## Scope and limitations
42
+
43
+ Claims come from root README.md and docs/**/*.md prose, including inline code.
44
+ Backtick and tilde fenced code is excluded; sentences containing negation are
45
+ suppressed. Positive sentences elsewhere can independently establish a claim.
46
+ UTF-8, utf8, utf_8, and Unicode are recognized case-insensitively.
47
+
48
+ AST checks cover open/io.open, codecs.open, io.TextIOWrapper, and read_text/
49
+ write_text methods. Missing or non-UTF-8 literal encodings are evidence.
50
+ Standard open positional encoding and codecs.open third positional encoding
51
+ are recognized; Path methods and TextIOWrapper require keyword encoding.
52
+ Binary I/O is excluded. Tests are identified by tests/test/fixtures directory
53
+ components. examples directories and symlinks are skipped. Only Python source
54
+ and Markdown documentation are read; .env contents are never opened.
55
+ No claims yields empty findings and verified claims. No Python source yields
56
+ HIGH per family. No production text I/O makes the claim moot, not falsified.
57
+
58
+ This is static pattern matching, not semantic judgement. Dead code counts.
59
+ Variable modes/encodings are skipped as unknown. Dynamic file opens are missed.
60
+ Alias tracking is per-file only; shadowing and reassignment are not resolved.
61
+ Text method matching does not infer receiver types and may flag unrelated methods.
62
+ Negation is conservative and sentence based, so unrelated negative words can
63
+ suppress claims. Clean results with unknown encodings are qualified rather than
64
+ proof of runtime behavior. Source must be UTF-8 (optional BOM), docs UTF-8;
65
+ malformed or unreadable input yields exit 1.
66
+
67
+ ## Differentiation
68
+
69
+ - privacy-claim-audit: network/telemetry evidence, never text encodings.
70
+ - config-claim-audit: documented default values, never encoding arguments.
71
+ - readme-claim-audit: install/flag/link/version, never encoding.
72
+ - bandit/ruff: code issues, never documentation promises.
73
+ - file/chardet: actual bytes on disk, never documentation versus code truth.
74
+
75
+ ## Development
76
+
77
+ ```sh
78
+ PYTHONPATH=src python3 -m unittest discover -s tests -v
79
+ python3 -m build
80
+ ```
81
+
82
+ The source distribution includes tests and good/bad/testonly fixtures, including
83
+ a marker-only .env read guard. Licensed under MIT.
@@ -0,0 +1,10 @@
1
+ encoding_claim_audit/__init__.py,sha256=BZoVM8SxwB_k43aw_jbMSOogMX3Iumm8-UCRbrR3U8o,74
2
+ encoding_claim_audit/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ encoding_claim_audit/audit.py,sha256=l8UldmDYbMQA22AiV1C47M35z-P2VFhlXSXGikEZGBw,5616
4
+ encoding_claim_audit/cli.py,sha256=5SYPrt-kag0C0OJsqKoqjUSaPUadtVMqbNt_PGkbO7w,1939
5
+ encoding_claim_audit-0.1.0.dist-info/licenses/LICENSE,sha256=0wppIt49xM1AQe-H0_B0TVS6Rm8iRfGPI5p0aebGueM,1090
6
+ encoding_claim_audit-0.1.0.dist-info/METADATA,sha256=IhPzV0WZQUqStXX22yChjUSE4cXHrOvEJud7wXOzpk0,3740
7
+ encoding_claim_audit-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ encoding_claim_audit-0.1.0.dist-info/entry_points.txt,sha256=7m523uGzJJP6r-tzJKo2Ieui0kr8KJBKtXtogD6wBxU,71
9
+ encoding_claim_audit-0.1.0.dist-info/top_level.txt,sha256=LaTEuo01-CVUY_i6LwLVEdryNRVvMXmy1APdiRjJqTw,21
10
+ encoding_claim_audit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ encoding-claim-audit = encoding_claim_audit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 encoding-claim-audit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ encoding_claim_audit