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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
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
+ }
@@ -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,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,2 @@
1
+ [console_scripts]
2
+ pytest-deprecation-lint = readystack_pytest_deprecation_lint.cli:main