readystack-pytest-deprecation-lint 1.0.3__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.
- readystack_pytest_deprecation_lint-1.0.3/LICENSE.txt +16 -0
- readystack_pytest_deprecation_lint-1.0.3/PKG-INFO +79 -0
- readystack_pytest_deprecation_lint-1.0.3/README.md +60 -0
- readystack_pytest_deprecation_lint-1.0.3/pyproject.toml +28 -0
- readystack_pytest_deprecation_lint-1.0.3/setup.cfg +4 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/__init__.py +1 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/cli.py +135 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/engine.py +30 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/license.py +61 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/rules.json +139 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint/strings.json +10 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/PKG-INFO +79 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/SOURCES.txt +14 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/dependency_links.txt +1 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/entry_points.txt +2 -0
- readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Pytest Deprecation Lint (pytest 8 removals) — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Lint the pytest file you have open and see every removed API and every silently-ignored config key, each with the pytest version that removed it and the exact replacement line. is free for any use, personal or commercial, for as long as you keep the
|
|
5
|
+
extension installed, and the online check at the product page is free without registration.
|
|
6
|
+
|
|
7
|
+
Paid scope
|
|
8
|
+
Audit every test file, conftest.py and config file in the workspace in one pass and export the dated findings as Markdown or JSON you keep and paste into CI. is the paid part. It asks for a licence key issued at purchase (one seat, one key;
|
|
9
|
+
7-day full refund, no questions). The key is checked against the payment provider and
|
|
10
|
+
cached locally for 30 days so it keeps working offline.
|
|
11
|
+
|
|
12
|
+
Redistribution
|
|
13
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
14
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
15
|
+
|
|
16
|
+
(c) ReadyStack — https://getreadystack.com
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-pytest-deprecation-lint
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: pytest,deprecation,pytest 8,python testing,conftest,migration
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Pytest Deprecation Lint (pytest 8 removals)
|
|
21
|
+
|
|
22
|
+
Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pytest-deprecation-lint file
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Node 18+. The same 19 rules as the VS Code extension, from a terminal or CI.
|
|
31
|
+
|
|
32
|
+
## Free
|
|
33
|
+
|
|
34
|
+
- Lint the test file, conftest.py or pytest config you have open and see every removed API and every silently ignored config key, each with the pytest version that removed it and the exact replacement line.
|
|
35
|
+
- `--rules` lists every rule
|
|
36
|
+
|
|
37
|
+
## With a licence ($29 once)
|
|
38
|
+
|
|
39
|
+
- Sweep every test file, conftest.py and config file in the workspace in one pass and export the dated findings as Markdown or JSON you keep and paste into a CI job or a migration ticket.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
@readystack/pytest-deprecation-lint --dir ./templates --report html --out report.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A contract Python engineer reviewing the same suite for pytest 8 breakage by hand bills roughly $80-$120 an hour in the US market.
|
|
46
|
+
|
|
47
|
+
## Use from an AI agent (MCP)
|
|
48
|
+
|
|
49
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "mcpServers": { "pytest-deprecation-lint": { "command": "npx", "args": ["-y", "@readystack/pytest-deprecation-lint", "--mcp"] } } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
|
|
56
|
+
|
|
57
|
+
## Use in CI
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- name: Pytest Deprecation Lint (pytest 8 removals)
|
|
61
|
+
run: npx -y @readystack/pytest-deprecation-lint --dir . --ci
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/pytest-deprecation-lint --dir /work --ci`)
|
|
65
|
+
|
|
66
|
+
Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
67
|
+
|
|
68
|
+
[Get a licence](https://buy.polar.sh/polar_cl_OR0dShZDbbiVxsy2XNVTaEV9nNVJY9nD92Tgf3TsG1F)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
<!-- pytest deprecation lint -->
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Install (PyPI)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install readystack-pytest-deprecation-lint
|
|
78
|
+
pytest-deprecation-lint --help
|
|
79
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Pytest Deprecation Lint (pytest 8 removals)
|
|
2
|
+
|
|
3
|
+
Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pytest-deprecation-lint file
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+. The same 19 rules as the VS Code extension, from a terminal or CI.
|
|
12
|
+
|
|
13
|
+
## Free
|
|
14
|
+
|
|
15
|
+
- Lint the test file, conftest.py or pytest config you have open and see every removed API and every silently ignored config key, each with the pytest version that removed it and the exact replacement line.
|
|
16
|
+
- `--rules` lists every rule
|
|
17
|
+
|
|
18
|
+
## With a licence ($29 once)
|
|
19
|
+
|
|
20
|
+
- Sweep every test file, conftest.py and config file in the workspace in one pass and export the dated findings as Markdown or JSON you keep and paste into a CI job or a migration ticket.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
@readystack/pytest-deprecation-lint --dir ./templates --report html --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A contract Python engineer reviewing the same suite for pytest 8 breakage by hand bills roughly $80-$120 an hour in the US market.
|
|
27
|
+
|
|
28
|
+
## Use from an AI agent (MCP)
|
|
29
|
+
|
|
30
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "mcpServers": { "pytest-deprecation-lint": { "command": "npx", "args": ["-y", "@readystack/pytest-deprecation-lint", "--mcp"] } } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
|
|
37
|
+
|
|
38
|
+
## Use in CI
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- name: Pytest Deprecation Lint (pytest 8 removals)
|
|
42
|
+
run: npx -y @readystack/pytest-deprecation-lint --dir . --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/pytest-deprecation-lint --dir /work --ci`)
|
|
46
|
+
|
|
47
|
+
Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
48
|
+
|
|
49
|
+
[Get a licence](https://buy.polar.sh/polar_cl_OR0dShZDbbiVxsy2XNVTaEV9nNVJY9nD92Tgf3TsG1F)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<!-- pytest deprecation lint -->
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Install (PyPI)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
pip install readystack-pytest-deprecation-lint
|
|
59
|
+
pytest-deprecation-lint --help
|
|
60
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "readystack-pytest-deprecation-lint"
|
|
7
|
+
version = "1.0.3"
|
|
8
|
+
description = "Marks the lines in your test suite that current pytest no longer runs \u2014 and the config keys it silently ignores."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "Proprietary - see LICENSE.txt"}
|
|
12
|
+
keywords = ["pytest", "deprecation", "pytest 8", "python testing", "conftest", "migration"]
|
|
13
|
+
authors = [{name = "ReadyStack", email = "support@getreadystack.com"}]
|
|
14
|
+
classifiers = ["Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Software Development :: Quality Assurance", "Operating System :: OS Independent"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi"
|
|
18
|
+
Documentation = "https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi"
|
|
19
|
+
Support = "https://getreadystack.com/support"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
"pytest-deprecation-lint" = "readystack_pytest_deprecation_lint.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
"readystack_pytest_deprecation_lint" = ["rules.json", "strings.json"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.3'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# cli.py — the same rules as the VS Code extension, from a terminal, a container or an MCP client. Generated by pypi_build.py.
|
|
2
|
+
import json, os, sys, time, datetime
|
|
3
|
+
from . import engine as ENGINE, license as lic
|
|
4
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
5
|
+
S = json.load(open(os.path.join(_HERE, 'strings.json'), encoding='utf-8'))
|
|
6
|
+
def scan(text, file=''):
|
|
7
|
+
r = ENGINE.check(str(text), {'today': datetime.date.today().isoformat(), 'path': file})
|
|
8
|
+
return [{'line': int(f.get('line') or 1), 'msg': str(f.get('msg') or f.get('check') or ''), 'fix': f.get('fix'), 'sev': 'error' if str(f.get('sev', 'error')).lower().startswith('err') else 'warn'} for f in r.get('findings', [])]
|
|
9
|
+
def is_text(p):
|
|
10
|
+
try:
|
|
11
|
+
with open(p, 'rb') as h: b = h.read(4096)
|
|
12
|
+
return os.path.getsize(p) <= 2 * 1024 * 1024 and b'\x00' not in b
|
|
13
|
+
except Exception: return False
|
|
14
|
+
def walk(d, exts, out):
|
|
15
|
+
try: ents = list(os.scandir(d))
|
|
16
|
+
except Exception: return out
|
|
17
|
+
for e in ents:
|
|
18
|
+
if e.name in ('node_modules', '.git') or e.name.startswith('.'): continue
|
|
19
|
+
if e.is_dir(): walk(e.path, exts, out)
|
|
20
|
+
elif (not exts or os.path.splitext(e.name)[1].lower() in exts) and is_text(e.path): out.append(e.path)
|
|
21
|
+
return out
|
|
22
|
+
def esc(s): return str(s).replace('&', '&').replace('<', '<').replace('>', '>')
|
|
23
|
+
def csvq(s): return '"' + str('' if s is None else s).replace('"', '""') + '"'
|
|
24
|
+
RULE_N = len(ENGINE.RULES)
|
|
25
|
+
def render(rows, fmt):
|
|
26
|
+
if fmt == 'json': return json.dumps({'tool': S['name'], 'rules': RULE_N, 'files': rows}, indent=1)
|
|
27
|
+
if fmt == 'csv':
|
|
28
|
+
o = ['file,line,severity,message,fix']
|
|
29
|
+
for r in rows:
|
|
30
|
+
for h in r['hits']: o.append(','.join([csvq(r['file']), str(h['line']), h['sev'], csvq(h['msg']), csvq(h['fix'])]))
|
|
31
|
+
return '\n'.join(o)
|
|
32
|
+
if fmt == 'html':
|
|
33
|
+
n = sum(len(r['hits']) for r in rows)
|
|
34
|
+
o = '<!doctype html><meta charset="utf-8"><title>' + esc(S['name']) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px}</style>'
|
|
35
|
+
o += '<h1>' + esc(S['name']) + '</h1><p>' + str(len(rows)) + ' files · ' + str(n) + ' findings · ' + str(RULE_N) + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>'
|
|
36
|
+
for r in rows:
|
|
37
|
+
for h in r['hits']: o += '<tr><td><code>' + esc(r['file']) + '</code></td><td>' + str(h['line']) + '</td><td>' + esc(h['sev']) + '</td><td>' + esc(h['msg']) + '</td><td>' + esc(h['fix'] or '') + '</td></tr>'
|
|
38
|
+
return o + '</table>'
|
|
39
|
+
o = ''
|
|
40
|
+
for r in rows:
|
|
41
|
+
o += r['file'] + '\n'
|
|
42
|
+
for h in r['hits']: o += ' ' + str(h['line']).rjust(5) + ' ' + h['sev'].ljust(5) + ' ' + h['msg'] + ('\n -> ' + h['fix'] if h['fix'] else '') + '\n'
|
|
43
|
+
return o
|
|
44
|
+
def trial_state():
|
|
45
|
+
if os.environ.get('READYSTACK_NO_TRIAL'): return {'active': False}
|
|
46
|
+
try:
|
|
47
|
+
p = os.path.join(os.path.dirname(lic.store_path()), S['bin'] + '.trial.json'); t = None
|
|
48
|
+
try: t = json.load(open(p, encoding='utf-8'))
|
|
49
|
+
except Exception: pass
|
|
50
|
+
if not t or not t.get('until'):
|
|
51
|
+
t = {'until': (datetime.date.today() + datetime.timedelta(days=7)).isoformat()}; os.makedirs(os.path.dirname(p), exist_ok=True); json.dump(t, open(p, 'w', encoding='utf-8'))
|
|
52
|
+
return {'active': datetime.date.fromisoformat(t['until']) >= datetime.date.today(), 'until': t['until']}
|
|
53
|
+
except Exception: return {'active': False}
|
|
54
|
+
def mcp_serve():
|
|
55
|
+
os.environ['READYSTACK_MCP'] = '1'
|
|
56
|
+
def send(o): sys.stdout.write(json.dumps(o) + '\n'); sys.stdout.flush()
|
|
57
|
+
tools = [
|
|
58
|
+
{'name': 'check_text', 'description': S['name'] + ' - run all ' + str(RULE_N) + ' checks on a text (free)', 'inputSchema': {'type': 'object', 'properties': {'text': {'type': 'string', 'description': 'file contents'}, 'path': {'type': 'string'}}, 'required': ['text']}},
|
|
59
|
+
{'name': 'check_file', 'description': S['name'] + ' - run all checks on one file by path (free)', 'inputSchema': {'type': 'object', 'properties': {'path': {'type': 'string'}}, 'required': ['path']}},
|
|
60
|
+
{'name': 'check_dir', 'description': S['name'] + ' - sweep a folder and return every finding (licence; the full run is free for 7 days)', 'inputSchema': {'type': 'object', 'properties': {'dir': {'type': 'string'}, 'ext': {'type': 'string'}}, 'required': ['dir']}},
|
|
61
|
+
]
|
|
62
|
+
def result(i, rows): send({'jsonrpc': '2.0', 'id': i, 'result': {'content': [{'type': 'text', 'text': render(rows, 'text')}], 'structuredContent': {'tool': S['name'], 'rules': RULE_N, 'files': rows}}})
|
|
63
|
+
def fail(i, text): send({'jsonrpc': '2.0', 'id': i, 'result': {'content': [{'type': 'text', 'text': text}], 'isError': True}})
|
|
64
|
+
for line in sys.stdin:
|
|
65
|
+
try: m = json.loads(line)
|
|
66
|
+
except Exception: continue
|
|
67
|
+
i, method = m.get('id'), m.get('method')
|
|
68
|
+
if method == 'initialize': send({'jsonrpc': '2.0', 'id': i, 'result': {'protocolVersion': '2025-06-18', 'capabilities': {'tools': {}}, 'serverInfo': {'name': 'readystack-' + S['bin'], 'version': '1.0.3'}}}); continue
|
|
69
|
+
if method in ('notifications/initialized', 'ping'):
|
|
70
|
+
if i is not None: send({'jsonrpc': '2.0', 'id': i, 'result': {}})
|
|
71
|
+
continue
|
|
72
|
+
if method == 'tools/list': send({'jsonrpc': '2.0', 'id': i, 'result': {'tools': tools}}); continue
|
|
73
|
+
if method == 'tools/call':
|
|
74
|
+
name = (m.get('params') or {}).get('name'); args = (m.get('params') or {}).get('arguments') or {}
|
|
75
|
+
try:
|
|
76
|
+
if name == 'check_text': result(i, [{'file': args.get('path') or '(text)', 'hits': scan(str(args.get('text') or ''), args.get('path') or '')}]); continue
|
|
77
|
+
if name == 'check_file': result(i, [{'file': args['path'], 'hits': scan(open(args['path'], encoding='utf-8', errors='replace').read(), args['path'])}]); continue
|
|
78
|
+
if name == 'check_dir':
|
|
79
|
+
r = lic.ensure()
|
|
80
|
+
if not r['ok'] and not trial_state()['active']: fail(i, S['need_key'] + ' Get a licence ($' + str(S['price']) + ', once, 7-day refund): ' + lic.BUY_URL); continue
|
|
81
|
+
files = walk(args['dir'], [args['ext']] if args.get('ext') else (S.get('exts') or []), [])
|
|
82
|
+
rows = []
|
|
83
|
+
for f in files:
|
|
84
|
+
try: rows.append({'file': f, 'hits': scan(open(f, encoding='utf-8', errors='replace').read(), f)})
|
|
85
|
+
except Exception as e: rows.append({'file': f, 'hits': [], 'error': str(e)})
|
|
86
|
+
result(i, rows); continue
|
|
87
|
+
send({'jsonrpc': '2.0', 'id': i, 'error': {'code': -32601, 'message': 'unknown tool ' + str(name)}})
|
|
88
|
+
except Exception as e: fail(i, str(e))
|
|
89
|
+
continue
|
|
90
|
+
if i is not None: send({'jsonrpc': '2.0', 'id': i, 'error': {'code': -32601, 'message': 'method not found: ' + str(method)}})
|
|
91
|
+
def help_text():
|
|
92
|
+
b = S['bin']
|
|
93
|
+
return '\n'.join([S['name'] + ' - ' + S['subtitle'], '', 'Usage: ' + b + ' <file> [more files] check the files you name (free, every rule)',
|
|
94
|
+
' ' + b + ' --dir <folder> [--ext .py] scan a whole folder (licence)',
|
|
95
|
+
' ' + b + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
96
|
+
' ' + b + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
97
|
+
' ' + b + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
98
|
+
' ' + b + ' --rules list the ' + str(RULE_N) + ' rules',
|
|
99
|
+
' ' + b + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
100
|
+
'Free: ' + S['free'], 'Licence ($' + str(S['price']) + ', once, 7-day refund): ' + S['paid'], 'Get a licence: ' + lic.BUY_URL, ''])
|
|
101
|
+
def main(argv=None):
|
|
102
|
+
a = list(sys.argv[1:] if argv is None else argv)
|
|
103
|
+
try:
|
|
104
|
+
feed = lic.pull_feed()
|
|
105
|
+
if feed and isinstance(feed.get('rules'), list): ENGINE.RULES.extend(feed['rules'])
|
|
106
|
+
except Exception: pass
|
|
107
|
+
def get(k):
|
|
108
|
+
return a[a.index(k) + 1] if k in a and a.index(k) + 1 < len(a) else None
|
|
109
|
+
if '--mcp' in a: mcp_serve(); return 0
|
|
110
|
+
if not a or '--help' in a or '-h' in a: sys.stdout.write(help_text()); return 0
|
|
111
|
+
if '--rules' in a:
|
|
112
|
+
sys.stdout.write('\n'.join(str(i + 1).rjust(3) + ' [' + str(r.get('sev', 'warn')) + '] ' + str(r.get('msg') or r.get('id') or '') for i, r in enumerate(ENGINE.RULES)) + '\n'); return 0
|
|
113
|
+
if '--license' in a:
|
|
114
|
+
r = lic.ensure(get('--license')); sys.stdout.write(('Licence stored: ' + lic.store_path() if r['ok'] else 'Licence not accepted (' + str(r.get('why')) + ')') + '\n'); return 0 if r['ok'] else 1
|
|
115
|
+
d, fmt, out, ci = get('--dir'), get('--report'), get('--out'), '--ci' in a
|
|
116
|
+
exts = [get('--ext')] if '--ext' in a else (S.get('exts') or [])
|
|
117
|
+
if d or fmt or ci:
|
|
118
|
+
r = lic.ensure()
|
|
119
|
+
if not r['ok']:
|
|
120
|
+
t = trial_state()
|
|
121
|
+
if t['active']: sys.stderr.write('Trial: the full run is free until ' + t['until'] + ' - after that $' + str(S['price']) + ' once (7-day refund). Get a licence: ' + lic.BUY_URL + '\n')
|
|
122
|
+
else: sys.stderr.write(S['need_key'] + '\n set READYSTACK_LICENSE=<key> or ' + S['bin'] + ' --license <key>\n Get a licence ($' + str(S['price']) + ', once): ' + lic.BUY_URL + '\n'); return 2
|
|
123
|
+
flags = {'--dir', '--report', '--out', '--ext', '--license'}
|
|
124
|
+
files = walk(d, exts, []) if d else [x for i, x in enumerate(a) if not x.startswith('--') and (i == 0 or a[i - 1] not in flags)]
|
|
125
|
+
if not files: sys.stderr.write('No files. ' + S['bin'] + ' --help\n'); return 2
|
|
126
|
+
rows = []
|
|
127
|
+
for f in files:
|
|
128
|
+
try: rows.append({'file': f, 'hits': scan(open(f, encoding='utf-8', errors='replace').read(), f)})
|
|
129
|
+
except Exception as e: rows.append({'file': f, 'hits': [], 'error': str(e)})
|
|
130
|
+
text = render(rows, fmt or 'text')
|
|
131
|
+
if out: open(out, 'w', encoding='utf-8').write(text)
|
|
132
|
+
else: sys.stdout.write(text if text.endswith('\n') else text + '\n')
|
|
133
|
+
errors = sum(1 for r in rows for h in r['hits'] if h['sev'] == 'error')
|
|
134
|
+
return 1 if (ci and errors) else 0
|
|
135
|
+
if __name__ == '__main__': sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# engine.py — same rules as the VS Code extension and the npm CLI (rules.json), pure Python. Generated by pypi_build.py.
|
|
2
|
+
import json, os, re
|
|
3
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
4
|
+
RULES = json.load(open(os.path.join(_HERE, 'rules.json'), encoding='utf-8'))
|
|
5
|
+
def _py(p): return re.sub(r'\(\?<(?![=!])', '(?P<', p)
|
|
6
|
+
def _c(r):
|
|
7
|
+
if '_re' not in r:
|
|
8
|
+
r['_re'] = re.compile(_py(r['re'])); r['_ctx'] = re.compile(_py(r['ctx']), re.M) if r.get('ctx') else None
|
|
9
|
+
r['_prev'] = re.compile(_py(r['prev_not'])) if r.get('prev_not') else None; r['_next'] = re.compile(_py(r['next_re'])) if r.get('next_re') else None
|
|
10
|
+
return r
|
|
11
|
+
def _code(s): t = s.strip(); return t != '' and not t.startswith('#')
|
|
12
|
+
def check(text, opts=None):
|
|
13
|
+
opts = opts or {}; src = '' if text is None else str(text); lines = src.split('\n'); lines = [l.rstrip('\r') for l in lines]; out = []
|
|
14
|
+
for r in RULES:
|
|
15
|
+
r = _c(r)
|
|
16
|
+
if r['_ctx'] and not r['_ctx'].search(src): continue
|
|
17
|
+
for n, line in enumerate(lines):
|
|
18
|
+
if not _code(line) or not r['_re'].search(line): continue
|
|
19
|
+
if r['_prev']:
|
|
20
|
+
p = n - 1
|
|
21
|
+
while p >= 0 and lines[p].strip() == '': p -= 1
|
|
22
|
+
if p >= 0 and r['_prev'].search(lines[p]): continue
|
|
23
|
+
if r['_next']:
|
|
24
|
+
q = n + 1
|
|
25
|
+
while q < len(lines) and lines[q].strip() == '': q += 1
|
|
26
|
+
if q >= len(lines) or not r['_next'].search(lines[q]): continue
|
|
27
|
+
out.append({'check': r['id'], 'sev': r.get('sev', 'warn'), 'line': n + 1, 'msg': r.get('msg', ''), 'fix': r.get('fix'), 'text': line.strip()[:160]})
|
|
28
|
+
out.sort(key=lambda f: f['line'])
|
|
29
|
+
return {'findings': out, 'scanned_lines': len(lines), 'rule_count': len(RULES),
|
|
30
|
+
'errors': sum(1 for f in out if f['sev'] == 'error'), 'warnings': sum(1 for f in out if f['sev'] == 'warn'), 'today': opts.get('today', '')}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# license.py — Polar licence check (same contract as license.js). Generated by pypi_build.py.
|
|
2
|
+
import json, os, sys, time, urllib.request
|
|
3
|
+
ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17'; BENEFIT_ID = '1a97f5b0-9a81-4251-9f7a-83126a289d10'; BUY_URL = 'https://buy.polar.sh/polar_cl_OR0dShZDbbiVxsy2XNVTaEV9nNVJY9nD92Tgf3TsG1F'; ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; SLUG = 'pytest-deprecation-lint'
|
|
4
|
+
GRACE = 30 * 86400; RECHECK = 7 * 86400
|
|
5
|
+
def store_path():
|
|
6
|
+
return os.path.join(os.environ.get('READYSTACK_HOME') or os.path.join(os.path.expanduser('~'), '.config', 'readystack'), SLUG + '.json')
|
|
7
|
+
def load():
|
|
8
|
+
try: return json.load(open(store_path(), encoding='utf-8'))
|
|
9
|
+
except Exception: return {}
|
|
10
|
+
def save(o):
|
|
11
|
+
try:
|
|
12
|
+
os.makedirs(os.path.dirname(store_path()), exist_ok=True); json.dump(o, open(store_path(), 'w', encoding='utf-8'))
|
|
13
|
+
except Exception: pass
|
|
14
|
+
def _post(url, body, headers, timeout):
|
|
15
|
+
req = urllib.request.Request(url, data=json.dumps(body).encode(), headers=dict({'content-type': 'application/json'}, **headers), method='POST')
|
|
16
|
+
return urllib.request.urlopen(req, timeout=timeout)
|
|
17
|
+
def _validate1(key, ben):
|
|
18
|
+
if not ORG_ID: return {'ok': False, 'offline': False}
|
|
19
|
+
body = {'key': key, 'organization_id': ORG_ID}
|
|
20
|
+
if len(ben) == 36: body['benefit_id'] = ben
|
|
21
|
+
try:
|
|
22
|
+
r = _post('https://api.polar.sh/v1/customer-portal/license-keys/validate', body, {'polar-version': '2026-04', 'user-agent': 'readystack-py/' + SLUG}, 8)
|
|
23
|
+
if r.status != 200: return {'ok': False, 'offline': False}
|
|
24
|
+
j = json.loads(r.read().decode() or '{}'); return {'ok': bool(j and (j.get('status') == 'granted' or j.get('valid') is True or j.get('id'))), 'offline': False}
|
|
25
|
+
except urllib.error.HTTPError: return {'ok': False, 'offline': False}
|
|
26
|
+
except Exception: return {'ok': False, 'offline': True}
|
|
27
|
+
def validate(key):
|
|
28
|
+
r = _validate1(key, BENEFIT_ID)
|
|
29
|
+
if r['ok'] or r['offline'] or len(ALL_BENEFIT_ID) != 36: return r
|
|
30
|
+
return _validate1(key, ALL_BENEFIT_ID)
|
|
31
|
+
_pinged = [False]
|
|
32
|
+
def ping_paywall(why):
|
|
33
|
+
try:
|
|
34
|
+
if _pinged[0]: return
|
|
35
|
+
_pinged[0] = True
|
|
36
|
+
if os.environ.get('DO_NOT_TRACK') == '1' or os.environ.get('READYSTACK_NO_TELEMETRY') or os.environ.get('CI'): return
|
|
37
|
+
if not (sys.stdout.isatty() or sys.stdin.isatty() or os.environ.get('READYSTACK_MCP') == '1'): return
|
|
38
|
+
_post('https://getreadystack.com/api/ev', {'t': 'paywall', 'slug': SLUG, 'src': 'mcp' if os.environ.get('READYSTACK_MCP') == '1' else 'cli', 'why': why or 'no_key'}, {'user-agent': 'readystack-py/' + SLUG}, 3).read()
|
|
39
|
+
except Exception: pass
|
|
40
|
+
def ensure(explicit=None):
|
|
41
|
+
st = load(); key = explicit or os.environ.get('READYSTACK_LICENSE') or st.get('key')
|
|
42
|
+
if not key: ping_paywall('no_key'); return {'ok': False, 'why': 'no_key'}
|
|
43
|
+
age = time.time() - (st.get('okAt') or 0)
|
|
44
|
+
if not explicit and st.get('key') == key and age < RECHECK: return {'ok': True, 'cached': True}
|
|
45
|
+
r = validate(str(key).strip())
|
|
46
|
+
if r['ok']: save({'key': str(key).strip(), 'okAt': time.time()}); return {'ok': True}
|
|
47
|
+
if r['offline'] and st.get('key') == key and age < GRACE: return {'ok': True, 'offline': True}
|
|
48
|
+
if not r['offline']: ping_paywall('invalid')
|
|
49
|
+
return {'ok': False, 'why': 'offline' if r['offline'] else 'invalid'}
|
|
50
|
+
def pull_feed():
|
|
51
|
+
st = load(); key = os.environ.get('READYSTACK_LICENSE') or st.get('key'); cached = st.get('feed')
|
|
52
|
+
if not key: return cached
|
|
53
|
+
if cached and time.time() - (st.get('feedAt') or 0) < RECHECK: return cached
|
|
54
|
+
try:
|
|
55
|
+
import urllib.parse
|
|
56
|
+
r = urllib.request.urlopen(urllib.request.Request('https://getreadystack.com/api/rules/' + urllib.parse.quote(SLUG) + '?key=' + urllib.parse.quote(key), headers={'user-agent': 'readystack-py'}), timeout=8)
|
|
57
|
+
if r.status != 200: return cached
|
|
58
|
+
j = json.loads(r.read().decode() or '{}')
|
|
59
|
+
if not isinstance(j, dict) or not isinstance(j.get('rules'), list): return cached
|
|
60
|
+
s2 = load(); s2['feed'] = j; s2['feedAt'] = time.time(); save(s2); return j
|
|
61
|
+
except Exception: return cached
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "warns_none",
|
|
4
|
+
"sev": "error",
|
|
5
|
+
"re": "pytest\\.warns\\s*\\(\\s*None\\s*\\)",
|
|
6
|
+
"msg": "pytest.warns(None) was removed in pytest 8.0 — it now raises instead of running the block.",
|
|
7
|
+
"fix": "pytest.warns(DeprecationWarning) for a specific class, or warnings.catch_warnings(record=True) to assert that nothing warned."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "nose_method_setup",
|
|
11
|
+
"sev": "error",
|
|
12
|
+
"re": "^\\s+def\\s+(setup|teardown)\\s*\\(\\s*self\\s*\\)\\s*:",
|
|
13
|
+
"msg": "nose-style setup(self)/teardown(self) methods stopped being called in pytest 8.0 — the body never runs and the test still reports green.",
|
|
14
|
+
"fix": "def setup_method(self, method): / def teardown_method(self, method):"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "nose_module_setup",
|
|
18
|
+
"sev": "error",
|
|
19
|
+
"re": "^def\\s+(setup|teardown)\\s*\\(\\s*\\)\\s*:",
|
|
20
|
+
"msg": "nose-style module-level setup()/teardown() stopped being called in pytest 8.0.",
|
|
21
|
+
"fix": "def setup_module(module): / def teardown_module(module):"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "pyproject_tool_pytest",
|
|
25
|
+
"sev": "error",
|
|
26
|
+
"re": "^\\s*\\[tool\\.pytest\\]\\s*$",
|
|
27
|
+
"msg": "[tool.pytest] is not a table pytest reads — every key under it (addopts, markers, filterwarnings, testpaths) is silently ignored and the suite still exits 0.",
|
|
28
|
+
"fix": "[tool.pytest.ini_options]"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "setupcfg_pytest_section",
|
|
32
|
+
"sev": "error",
|
|
33
|
+
"re": "^\\s*\\[pytest\\]\\s*$",
|
|
34
|
+
"ctx": "^\\s*\\[(metadata|options|flake8|isort|mypy|bdist_wheel)\\]",
|
|
35
|
+
"msg": "A [pytest] section in setup.cfg is no longer supported — pytest errors out or ignores it.",
|
|
36
|
+
"fix": "[tool:pytest]"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "yield_fixture",
|
|
40
|
+
"sev": "error",
|
|
41
|
+
"re": "@pytest\\.yield_fixture",
|
|
42
|
+
"msg": "pytest.yield_fixture was deprecated in pytest 3.0 and removed in pytest 6.2 — collection fails with AttributeError.",
|
|
43
|
+
"fix": "@pytest.fixture (a plain fixture may yield)"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "get_marker",
|
|
47
|
+
"sev": "error",
|
|
48
|
+
"re": "\\.get_marker\\s*\\(",
|
|
49
|
+
"msg": "Node.get_marker() was removed in pytest 4.0.",
|
|
50
|
+
"fix": "item.get_closest_marker(\"name\") or item.iter_markers(\"name\")"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "pytest_config_global",
|
|
54
|
+
"sev": "error",
|
|
55
|
+
"re": "\\bpytest\\.config\\b",
|
|
56
|
+
"msg": "The global pytest.config object was removed in pytest 5.0.",
|
|
57
|
+
"fix": "the pytestconfig fixture, or request.config inside a fixture/hook"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "raises_message",
|
|
61
|
+
"sev": "error",
|
|
62
|
+
"re": "pytest\\.raises\\([^)]*\\bmessage\\s*=",
|
|
63
|
+
"msg": "pytest.raises(..., message=...) was removed in pytest 5.0 — the kwarg is now a TypeError.",
|
|
64
|
+
"fix": "pytest.raises(Err, match=\"regex\")"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "funcarg_prefix",
|
|
68
|
+
"sev": "error",
|
|
69
|
+
"re": "^\\s*def\\s+pytest_funcarg__",
|
|
70
|
+
"msg": "The pytest_funcarg__ fixture prefix was removed in pytest 4.0 — the function is collected as nothing.",
|
|
71
|
+
"fix": "@pytest.fixture above a normally named function"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "pytest_namespace",
|
|
75
|
+
"sev": "error",
|
|
76
|
+
"re": "\\bpytest_namespace\\b",
|
|
77
|
+
"msg": "The pytest_namespace hook was removed in pytest 4.0 — the hook is never called.",
|
|
78
|
+
"fix": "import the helper from your own module, or expose it as a fixture"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "parametrize_on_testcase",
|
|
82
|
+
"sev": "error",
|
|
83
|
+
"re": "@pytest\\.mark\\.parametrize",
|
|
84
|
+
"ctx": "class\\s+\\w+\\s*\\(\\s*(unittest\\.)?TestCase\\s*\\)",
|
|
85
|
+
"msg": "@pytest.mark.parametrize does not work on unittest.TestCase subclasses — pytest refuses to inject the arguments, so the case never runs as written.",
|
|
86
|
+
"fix": "drop the TestCase base (a plain class works with pytest), or use TestCase.subTest"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "event_loop_fixture",
|
|
90
|
+
"sev": "error",
|
|
91
|
+
"re": "^\\s*def\\s+event_loop\\s*\\(",
|
|
92
|
+
"msg": "Redefining the event_loop fixture was deprecated in pytest-asyncio 0.23 and removed in pytest-asyncio 1.0 — the override is ignored.",
|
|
93
|
+
"fix": "override the event_loop_policy fixture, or set loop_scope= on the asyncio marker"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "async_test_unmarked",
|
|
97
|
+
"sev": "warn",
|
|
98
|
+
"re": "^\\s*async\\s+def\\s+test_",
|
|
99
|
+
"prev_not": "@pytest\\.mark\\.(asyncio|anyio)|@pytest\\.mark\\.parametrize",
|
|
100
|
+
"msg": "An unmarked async test is skipped with a warning unless pytest-asyncio runs in auto mode (asyncio_mode = \"auto\") — the assertions inside never execute.",
|
|
101
|
+
"fix": "@pytest.mark.asyncio above the test, or asyncio_mode = \"auto\" under [tool.pytest.ini_options]"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "tmpdir_fixture",
|
|
105
|
+
"sev": "warn",
|
|
106
|
+
"re": "\\btmpdir\\b",
|
|
107
|
+
"msg": "tmpdir hands back a py.path.local object from the unmaintained py library; pytest documents tmp_path as the replacement.",
|
|
108
|
+
"fix": "tmp_path (pathlib.Path) and the tmp_path_factory fixture"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "strict_flag",
|
|
112
|
+
"sev": "warn",
|
|
113
|
+
"re": "--strict(?![-\\w])",
|
|
114
|
+
"msg": "--strict is the deprecated spelling and is documented for removal.",
|
|
115
|
+
"fix": "--strict-markers (add --strict-config for config keys)"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "result_log",
|
|
119
|
+
"sev": "error",
|
|
120
|
+
"re": "--result-log",
|
|
121
|
+
"msg": "--result-log was removed in pytest 6.0 — the run aborts with \"unrecognized arguments\".",
|
|
122
|
+
"fix": "--junit-xml=report.xml, or the pytest-reportlog plugin"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "msg_kwarg",
|
|
126
|
+
"sev": "warn",
|
|
127
|
+
"re": "pytest\\.(skip|fail|exit)\\s*\\(\\s*msg\\s*=",
|
|
128
|
+
"msg": "The msg= keyword of pytest.skip/fail/exit was deprecated in pytest 7.0 in favour of reason=.",
|
|
129
|
+
"fix": "pytest.skip(reason=\"...\")"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "mark_on_fixture",
|
|
133
|
+
"sev": "warn",
|
|
134
|
+
"re": "^\\s*@pytest\\.mark\\.",
|
|
135
|
+
"next_re": "^\\s*@pytest\\.fixture",
|
|
136
|
+
"msg": "Marks applied to a fixture have no effect — pytest ignores them, so the skipif/parametrize you wrote there is dead.",
|
|
137
|
+
"fix": "move the mark onto the test, or branch inside the fixture with request.getfixturevalue"
|
|
138
|
+
}
|
|
139
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Pytest Deprecation Lint (pytest 8 removals)",
|
|
3
|
+
"subtitle": "Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.",
|
|
4
|
+
"bin": "pytest-deprecation-lint",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Lint the test file, conftest.py or pytest config you have open and see every removed API and every silently ignored config key, each with the pytest version that removed it and the exact replacement line.",
|
|
7
|
+
"paid": "Sweep every test file, conftest.py and config file in the workspace in one pass and export the dated findings as Markdown or JSON you keep and paste into a CI job or a migration ticket.",
|
|
8
|
+
"need_key": "This option needs a licence (Pytest Deprecation Lint (pytest 8 removals)).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|
readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-pytest-deprecation-lint
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/pytest-deprecation-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: pytest,deprecation,pytest 8,python testing,conftest,migration
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Pytest Deprecation Lint (pytest 8 removals)
|
|
21
|
+
|
|
22
|
+
Marks the lines in your test suite that current pytest no longer runs — and the config keys it silently ignores.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pytest-deprecation-lint file
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Node 18+. The same 19 rules as the VS Code extension, from a terminal or CI.
|
|
31
|
+
|
|
32
|
+
## Free
|
|
33
|
+
|
|
34
|
+
- Lint the test file, conftest.py or pytest config you have open and see every removed API and every silently ignored config key, each with the pytest version that removed it and the exact replacement line.
|
|
35
|
+
- `--rules` lists every rule
|
|
36
|
+
|
|
37
|
+
## With a licence ($29 once)
|
|
38
|
+
|
|
39
|
+
- Sweep every test file, conftest.py and config file in the workspace in one pass and export the dated findings as Markdown or JSON you keep and paste into a CI job or a migration ticket.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
@readystack/pytest-deprecation-lint --dir ./templates --report html --out report.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A contract Python engineer reviewing the same suite for pytest 8 breakage by hand bills roughly $80-$120 an hour in the US market.
|
|
46
|
+
|
|
47
|
+
## Use from an AI agent (MCP)
|
|
48
|
+
|
|
49
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "mcpServers": { "pytest-deprecation-lint": { "command": "npx", "args": ["-y", "@readystack/pytest-deprecation-lint", "--mcp"] } } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
|
|
56
|
+
|
|
57
|
+
## Use in CI
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- name: Pytest Deprecation Lint (pytest 8 removals)
|
|
61
|
+
run: npx -y @readystack/pytest-deprecation-lint --dir . --ci
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/pytest-deprecation-lint --dir /work --ci`)
|
|
65
|
+
|
|
66
|
+
Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
67
|
+
|
|
68
|
+
[Get a licence](https://buy.polar.sh/polar_cl_OR0dShZDbbiVxsy2XNVTaEV9nNVJY9nD92Tgf3TsG1F)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
<!-- pytest deprecation lint -->
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Install (PyPI)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install readystack-pytest-deprecation-lint
|
|
78
|
+
pytest-deprecation-lint --help
|
|
79
|
+
```
|
readystack_pytest_deprecation_lint-1.0.3/src/readystack_pytest_deprecation_lint.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/readystack_pytest_deprecation_lint/__init__.py
|
|
5
|
+
src/readystack_pytest_deprecation_lint/cli.py
|
|
6
|
+
src/readystack_pytest_deprecation_lint/engine.py
|
|
7
|
+
src/readystack_pytest_deprecation_lint/license.py
|
|
8
|
+
src/readystack_pytest_deprecation_lint/rules.json
|
|
9
|
+
src/readystack_pytest_deprecation_lint/strings.json
|
|
10
|
+
src/readystack_pytest_deprecation_lint.egg-info/PKG-INFO
|
|
11
|
+
src/readystack_pytest_deprecation_lint.egg-info/SOURCES.txt
|
|
12
|
+
src/readystack_pytest_deprecation_lint.egg-info/dependency_links.txt
|
|
13
|
+
src/readystack_pytest_deprecation_lint.egg-info/entry_points.txt
|
|
14
|
+
src/readystack_pytest_deprecation_lint.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
readystack_pytest_deprecation_lint
|