कॉन्फ़िगरेशन और चेतावनियाँ — नियम एक फ़ाइल में
हर रन पर वही फ़्लैग टाइप करना बंद कीजिए: प्रोजेक्ट के नियम pyproject.toml या pytest.ini में लिखिए — testpaths, addopts, pythonpath, markers, xfail_strict, log_cli — और pytest.warns, deprecated_call व filterwarnings से चेतावनियों को टेस्ट और नियंत्रित कीजिए।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
shop प्रोजेक्ट अब उस आकार में आ गया है जो ज़्यादातर असली प्रोजेक्ट्स का होता है: कोड src/ में, टेस्ट tests/ में।
shop/
├── pyproject.toml
├── src/
│ └── shop/
│ ├── __init__.py
│ └── pricing.py
└── tests/
└── test_pricing.pyआपने इसे सही तरीके से चलाना सीख लिया है, और "सही तरीका" अब एक लंबी लाइन बन चुका है:
PYTHONPATH=src pytest -ra --strict-markers testsएक साथी repository को clone करता है और वही टाइप करता है जो कोई भी करेगा:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 0 items / 1 error
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_pricing.py ____________________
ImportError while importing test module '/home/you/shop/tests/test_pricing.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_pricing.py:1: in <module>
from shop.pricing import line_total
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR tests/test_pricing.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.01s ===============================कोड में कोई गड़बड़ी नहीं है। गड़बड़ी यह है कि टेस्ट चलाने का तरीका प्रोजेक्ट में नहीं, बल्कि आपके दिमाग़ और आपकी shell history में रहता है। CI एक तीसरा रूप चलाएगा, और एक ही commit से तीन लोगों को तीन अलग नतीजे दिखेंगे।
ये flags आपकी पसंद नहीं हैं; ये प्रोजेक्ट के नियम हैं। यह अध्याय इन्हें एक ऐसी फ़ाइल में ले जाता है जिसे pytest हर बार पढ़ता है — और फिर उसी फ़ाइल से एक दूसरी, ज़्यादा चुपचाप रहने वाली समस्या सुलझाता है: वे warnings जो स्क्रीन पर से गुज़र जाती हैं और कभी पढ़ी नहीं जातीं।
इस अध्याय के अंत में आप कर पाएंगे
- pytest की settings को
pyproject.toml(याpytest.ini) में रखना और header से पक्का करना कि कौन-सी फ़ाइल इस्तेमाल हुई testpaths,addopts,pythonpath,minversion,markersऔरxfail_strictसोच-समझकर चुनना, और बता पाना कि क्यों- जब आपको किसी run को होते हुए देखना हो, तब
log_cliसे live logging चालू करना pytest.warnsऔरpytest.deprecated_callसे assert करना कि कोड warning देता है,matchऔर रिकॉर्ड की गई लिस्ट के साथ- config में
filterwarningsसे, mark के रूप में और-Wसे warnings को नियंत्रित करना — और CI में उन्हें error बनाना
ज़रूरी शर्तें: मार्कर, skip और xfail।
टेस्ट लिखने से पहले
यह अध्याय सामान्य से कम टेस्ट लिखता है और टेस्ट कैसे चलते हैं इसके बारे में ज़्यादा नियम। पहले होने वाली सोच वही है: तय करें कि क्या वादा किया गया है, फिर उन cases की योजना बनाएँ जो उस वादे को जाँचते हैं।
क्या वादा किया गया है। यहाँ दो तरह के वादे हैं।
- suite कैसे चलता है। जो कोई भी सादा
pytestटाइप करे, उसे वही टेस्ट, वही import path और वही सख़्ती मिले जो CI को मिलती है। गलत लिखा marker run को रोक दे। जोxfailपास होने लगे, उसकी रिपोर्ट हो, वह छिपा न रहे। - कोड अपनी return value के अलावा क्या कहता है।
apply_discount(total, percent)एक संख्या लौटाता है, औरpercent > 50होने पर यह एकUserWarningभी देता है कि discount "looks unusually large"।total_with_vat()अब भी काम करता है, लेकिन एकDeprecationWarningदेता है जोline_total()की ओर इशारा करती है। warning भी output है; जब वह contract का हिस्सा हो, तो उसे output की तरह ही टेस्ट किया जाता है।
क्या पहले से तैयार होना चाहिए। pytest 9 वाला एक virtual environment (pytest --version), कोड जो टेस्ट्स से import हो सके (यही नीचे pythonpath है), और प्रोजेक्ट के root पर एक config फ़ाइल। कोई अतिरिक्त plugin नहीं।
warning वाले व्यवहार के लिए योजना:
| केस | इनपुट | अपेक्षित | | --- | --- | --- | | सामान्य रास्ता | apply_discount(200.0, 10) | 180.0, कोई warning नहीं | | सीमा, अब भी शांत | apply_discount(200.0, 50) | 100.0, कोई warning नहीं | | सीमा के ठीक पार | apply_discount(200.0, 60) | 80.0 और एक UserWarning जिसमें "unusually large" हो | | deprecated नाम | total_with_vat(100.0, 1) | 115.0 और एक DeprecationWarning जिसमें line_total हो | | किसी library का अपना deprecation | कुछ भी जो उसे ट्रिगर करे | हमारा suite fail न करे |
config के लिए "टेस्ट" खुद एक run है: root से और tests/ के अंदर से, दोनों जगह सादा pytest केवल tests/ को collect करे और पास हो, और गलत लिखा marker run को रोक दे।
क्या टेस्ट नहीं करना है। यह नहीं कि Python का warnings module काम करता है, न यह कि pytest अपना config पढ़ता है — वह किसी और का, पहले से टेस्ट किया हुआ कोड है। warning का पूरा टेक्स्ट भी नहीं: वह वाक्यांश match करें जिस पर पढ़ने वाला कार्रवाई करता है ("unusually large"), ताकि बाकी शब्द बदलने से कुछ न टूटे। और किसी third-party library के deprecations भी नहीं — उन्हें filter किया जाता है, जो configuration का फ़ैसला है, टेस्ट नहीं।
सीमा वाली पंक्ति वही है जिसे लोग भूल जाते हैं, क्योंकि गायब warning कोई आवाज़ नहीं करती। अध्याय के अंत तक config की एक लाइन इसे एक ऐसी जाँच बना देगी जो आपको मुफ़्त में मिलती है।
एक फ़ाइल, जो सबसे पहले पढ़ी जाती है
pytest एक भी टेस्ट collect करने से पहले अपनी settings पढ़ता है। आमतौर पर इनकी जगह वह pyproject.toml होती है जो प्रोजेक्ट में पहले से है, [tool.pytest.ini_options] नाम की एक table में:
[project]
name = "shop"
version = "0.1.0"
[tool.pytest.ini_options]
pythonpath = ["src"]pythonpath उन directories की सूची है, प्रोजेक्ट root के सापेक्ष, जिन्हें pytest टेस्ट import करने से पहले sys.path में जोड़ता है। जिस कोड को टेस्ट किया जा रहा है वह साधारण है:
def line_total(price, quantity):
if quantity < 1:
raise ValueError("quantity must be at least 1")
return round(price * quantity * 1.15, 2)$ PYTHONPATH=src python -c "from shop.pricing import line_total; print(line_total(15.0, 3))"
51.75
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
collected 2 items
tests/test_pricing.py .. [100%]
============================== 2 passed in 0.01s ===============================header पढ़ें। configfile: pyproject.toml के ज़रिए pytest आपको बताता है कि उसने कौन-सी फ़ाइल इस्तेमाल की। जब भी लगे कि settings अनदेखी हो रही हैं, सबसे पहले यही लाइन देखें।
import को टेस्ट फ़ाइल के अंदर ही ठीक क्यों न कर दें? यह स्पष्ट रूप से गलत तरीका है, और यह काम करता है — कभी-कभी:
import sys
sys.path.insert(0, "src") # relative to wherever pytest was started
from shop.pricing import line_total
def test_three_items():
assert line_total(15.0, 3) == 51.75$ pytest -q
. [100%]
1 passed in 0.01s
$ cd tests && pytest -q
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR test_pricing.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sवहाँ "src" मौजूदा directory के सापेक्ष है, इसलिए टेस्ट पास होगा या टूटेगा, यह इस पर निर्भर करता है कि आप कहाँ खड़े हैं। config में pythonpath प्रोजेक्ट root के सापेक्ष है, जो अपनी जगह से नहीं हिलता। import की व्यवस्था प्रोजेक्ट की ज़िम्मेदारी है, इसलिए वह एक बार प्रोजेक्ट की फ़ाइल में जाती है, हर टेस्ट module के ऊपर नहीं।
कौन-सी फ़ाइल जीतती है, और root कहाँ है
pytest कई फ़ाइलें स्वीकार करता है। हर directory में वह इस क्रम में देखता है: pytest.toml, .pytest.toml, pytest.ini, .pytest.ini, pyproject.toml, tox.ini, setup.cfg। वह command line पर दिए गए paths (या मौजूदा directory) से शुरू करता है और ऊपर की ओर चलता है; पहली फ़ाइल जिसमें सचमुच pytest section हो, वही जीतती है। बिना pytest table वाली pyproject.toml गिनी नहीं जाती।
वह directory rootdir बन जाती है — header की rootdir: लाइन — और config में दिए relative paths उसी के सापेक्ष होते हैं। इसलिए tests/ के अंदर खड़े होकर भी root मिल जाता है:
$ cd tests && pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
collected 2 itemspytest.ini में यही settings INI syntax, एक [pytest] section, और बिना quotes या brackets के लिखी जाती हैं:
[pytest]
pythonpath = src
testpaths = tests
addopts = -ra --strict-markers
markers =
slow: takes more than a second; deselect with -m 'not slow'एक फ़ाइल चुनें। अगर दोनों मौजूद हों, तो pytest.ini पूरी तरह जीतती है — कुछ भी merge नहीं होता — और header यह बता देता है। यहाँ pyproject.toml के बगल में दो लाइनों वाली एक pytest.ini (minversion = 9.0) जोड़ी गई थी:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)
collected 0 items / 1 error…और ModuleNotFoundError वापस आ गया, क्योंकि pythonpath उस फ़ाइल में था जिसे अब अनदेखा किया जा रहा है।
testpaths — जब आप कुछ न बताएँ तब कहाँ देखना है
किसी ने rounding आज़माते हुए एक scratch फ़ाइल, notes/test_scratch.py, छोड़ दी। सादा pytest root के नीचे सब कुछ collect करता है, इसलिए वह भी चल जाती है:
$ pytest -q --tb=no
F.. [100%]
=========================== short test summary info ============================
FAILED notes/test_scratch.py::test_try_rounding - assert 2.67 == 2.68
1 failed, 2 passed in 0.01stestpaths बताता है कि असली suite कहाँ रहता है:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]$ pytest -q
.. [100%]
2 passed in 0.01sपूरे header में अब आपको testpaths: tests भी दिखेगा। यह एक default है, कोई बाड़ नहीं: यह तभी लागू होता है जब आप root से paths का नाम दिए बिना चलाते हैं, इसलिए pytest notes अब भी scratch फ़ाइल चलाता है। बड़े repository में यह collection को तेज़ भी करता है, क्योंकि pytest docs/, build/ या virtual environment वाले folder में घूमना बंद कर देता है।
addopts और markers — नियम जो सबको मिलते हैं
addopts वह टेक्स्ट है जिसे pytest आपके टाइप किए हुए से पहले जोड़ देता है। शुरुआत वाले प्रोजेक्ट के दोनों नियम यहाँ जाते हैं:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-ra --strict-markers"-ra हर उस टेस्ट के लिए एक छोटी summary लाइन प्रिंट करता है जो सीधे-सीधे पास नहीं हुआ — skipped, xfailed, xpassed, failed — ताकि कोई skip कभी अनदेखा न रह जाए। --strict-markers किसी unregistered marker को error बना देता है। एक नई फ़ाइल, tests/test_discounts.py, में एक टेस्ट @pytest.mark.slow से mark किया गया है (और एक Windows के अलावा बाकी जगह skip होता है), और marker अभी register नहीं हुआ है:
$ pytest -q
==================================== ERRORS ====================================
___________________ ERROR collecting tests/test_discounts.py ___________________
'slow' not found in `markers` configuration option
=========================== short test summary info ============================
ERROR tests/test_discounts.py - Failed: 'slow' not found in `markers` configu...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sयही इस flag का मकसद है। इसके बिना, @pytest.mark.slwo जैसी typo पर summary में बस एक PytestUnknownMarkWarning आती है, run हरा रहता है, और -m "not slow" उस टेस्ट को चलाता रहता है जिसे आप बाहर रखना चाहते थे। marker को register करें, हर एक के लिए एक लाइन, ऐसे विवरण के साथ जिसे pytest --markers दिखाएगा:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]$ pytest -q
.s.. [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_discounts.py:14: Windows rounding check
3 passed, 1 skipped in 0.01s
$ pytest --markers
@pytest.mark.slow: takes more than a second; deselect with -m 'not slow'SKIPPED वाली लाइन -ra का काम है: अब हर run पर skip खुद अपनी वजह बताता है।
addopts में क्या नहीं होना चाहिए। गलत तरीका है addopts = "-v -x --pdb"। ये आज की debugging के लिए आपकी आदतें हैं; साझा फ़ाइल में ये सब पर थोप दी जाती हैं, CI पर भी — जहाँ --pdb एक ऐसे keyboard का हमेशा इंतज़ार करता रहता है जिस पर कोई नहीं बैठा। addopts में केवल प्रोजेक्ट के नियम रखें। आप जो टाइप करते हैं वह उसके बाद जुड़ता है, इसलिए pytest -x अब भी सिर्फ़ आपके लिए काम करता है, और -o किसी एक run के लिए कोई भी setting override कर देता है:
$ pytest -q -o addopts=""
.s.. [100%]
3 passed, 1 skipped in 0.01sइस बार कोई skip लाइन नहीं, क्योंकि उस run के लिए -ra हटा दिया गया था।
minversion और xfail_strict
अगर किसी का pytest फ़ाइल के लिए बहुत पुराना है, तो minversion run को तुरंत रोक देता है। ऐसा version माँगने पर जो मौजूद ही नहीं, यह संदेश दिखता है:
$ pytest
ERROR: /home/you/mv/pytest.ini: 'minversion' requires pytest-10.0, actual pytest-9.1.1'शुरुआत में ही साफ़ error, उस पुराने pytest से बेहतर है जो settings को आधा-अधूरा समझे।
xfail_strict पिछले अध्याय का अधूरा छूटा सिरा संभालता है: एक xfail जो पास होने लगे। यहाँ एक bug को fail होने की उम्मीद के साथ mark किया गया था, और तब से किसी ने उसे ठीक कर दिया:
import pytest
def refund(amount):
return round(amount * 0.9, 2)
@pytest.mark.xfail(reason="bug #41: refunds lose a cent")
def test_refund_keeps_cents():
assert refund(10.0) == 9.0$ pytest -q
X [100%]
1 xpassed in 0.01sएक हरा run, एक X जिसे कोई नहीं पढ़ता, और एक marker जो अब झूठ बोल रहा है। config में xfail_strict = true के साथ:
$ pytest -q
F [100%]
=================================== FAILURES ===================================
___________________________ test_refund_keeps_cents ____________________________
[XPASS(strict)] bug #41: refunds lose a cent
=========================== short test summary info ============================
FAILED test_refunds.py::test_refund_keeps_cents - [XPASS(strict)] bug #41: re...
1 failed in 0.01sअच्छी ख़बर अब एक failure के रूप में आती है जो आपसे marker हटाने को कहती है — इसीलिए इसे टेस्ट-दर-टेस्ट नहीं, पूरे प्रोजेक्ट के लिए सेट करना सार्थक है। pytest 9.1 नया नाम strict_xfail भी स्वीकार करता है, और एक अकेला strict = true भी, जो सख़्ती के सारे विकल्प एक साथ चालू कर देता है (markers और xfail सहित); xfail_strict काम करता रहता है।
Live logs: log_cli
default रूप से pytest log records को capture करता है और उन्हें केवल fail होने वाले टेस्ट्स के लिए दिखाता है। किसी पास होते run की पूरी कहानी देखने के लिए live logging चालू करें:
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"तब हर टेस्ट के INFO और उससे ऊपर के records, टेस्ट चलते समय ही, live log call शीर्षक के नीचे प्रिंट होते हैं; नीचे का पूर्ण उदाहरण यह दिखाता है। Live logs शोर भरे होते हैं, इसलिए कई प्रोजेक्ट फ़ाइल में केवल log_cli_level रखते हैं और ज़रूरत पड़ने पर pytest -o log_cli=true से output चालू करते हैं।
pytest 9 की native table: [tool.pytest]
[tool.pytest.ini_options] का नाम अजीब है, और उसकी वजह है: ये TOML फ़ाइल में बैठी INI settings हैं, इसलिए हर value टेक्स्ट में बदल जाती है। pytest 9 एक native table, [tool.pytest], जोड़ता है, जहाँ values अपने TOML types बनाए रखती हैं — असली lists, असली booleans:
[tool.pytest]
minversion = "9.0"
pythonpath = ["src"]
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers"]
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
strict_xfail = true$ pytest -q
.s.. [100%]
=========================== short test summary info ============================
SKIPPED [1] tests/test_discounts.py:14: Windows rounding check
3 passed, 1 skipped in 0.01saddopts अब एक list है। दो नियम: एक फ़ाइल में एक ही table इस्तेमाल करें, दोनों कभी नहीं (pytest शुरू होने से मना कर देता है), और याद रखें कि pytest 9 से पहले के versions [tool.pytest] को बिल्कुल नहीं पढ़ते। यह अध्याय [tool.pytest.ini_options] रखता है क्योंकि यह हर उस pytest पर काम करता है जिससे आपका सामना होने की संभावना है; जो प्रोजेक्ट केवल pytest 9 चलाता है, उसके लिए native table ही अनुशंसित रूप है। pytest 9 एक अलग pytest.toml भी पढ़ता है, जिसमें इसी native शैली की [pytest] table होती है।
Cache, संक्षेप में
run के बाद root पर एक .pytest_cache/ folder होता है। यह याद रखता है कि पिछली बार कौन-से टेस्ट fail हुए थे, और इसी से --lf (last failed) और --ff (failed first) चलते हैं। दो flags जानने लायक हैं। pytest --cache-clear run से पहले इसे खाली कर देता है — तब उपयोगी जब --lf ऐसे टेस्ट चुनता रहे जो अब मौजूद ही नहीं हैं। pytest -p no:cacheprovider cache plugin को पूरी तरह बंद कर देता है, ताकि कुछ भी लिखा न जाए, जो read-only checkout के लिए ठीक है। कीमत यह है कि plugin के options भी उसके साथ ग़ायब हो जाते हैं:
$ pytest -q -p no:cacheprovider --lf
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --lf
inifile: /home/you/mv/pytest.ini
rootdir: /home/you/mvWarnings: run के अंत वाला section
warning एक संदेश है कि कुछ अभी गलत नहीं है। Python उसे प्रिंट करता है और आगे बढ़ जाता है:
import warnings
def line_total(price, quantity):
return round(price * quantity * 1.15, 2)
def total_with_vat(price, quantity):
warnings.warn(
"total_with_vat() is deprecated; use line_total()",
DeprecationWarning,
stacklevel=2,
)
return line_total(price, quantity)
def apply_discount(total, percent):
if percent > 50:
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
return round(total * (100 - percent) / 100, 2)$ python -c "from pricing import apply_discount; print(apply_discount(200.0, 60))"
/home/you/wn/pricing.py:19: UserWarning: discount of 60% looks unusually large
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
80.0stacklevel=2 deprecation को caller की लाइन की ओर इशारा करवाता है, यानी उस लाइन की ओर जिसे किसी को बदलना है। pytest टेस्ट्स के दौरान उठी warnings को पकड़ता है और उन्हें अंत में एक section में इकट्ठा कर देता है:
from pricing import total_with_vat
def test_old_name_still_works():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q
. [100%]
=============================== warnings summary ===============================
test_old_api.py::test_old_name_still_works
/home/you/wn/test_old_api.py:5: DeprecationWarning: total_with_vat() is deprecated; use line_total()
assert total_with_vat(100.0, 1) == 115.0
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 passed, 1 warning in 0.01sटेस्ट पास हुआ, और आख़िरी लाइन कहती है 1 passed, 1 warning। बड़े suite में यह section दर्जनों entries तक बढ़ जाता है, हर कोई इसे स्क्रॉल करके निकल जाना सीख लेता है, और एक दिन deprecated फ़ंक्शन हटा दिया जाता है और "पास होने वाला" suite टूट जाता है। warning के साथ आप दो काम कर सकते हैं: जब वह व्यवहार का हिस्सा हो, तो उसकी अपेक्षा करें, या जब न हो, तो उसे अस्वीकार करें।
pytest.warns — warning की अपेक्षा करना
जब warning contract का हिस्सा हो — योजना की "सीमा के ठीक पार" वाली पंक्ति — तो उसे वैसे ही टेस्ट करें जैसे आप pytest.raises से exception टेस्ट करते हैं:
import pytest
from pricing import apply_discount
def test_large_discount_warns():
with pytest.warns(UserWarning, match="unusually large"):
result = apply_discount(200.0, 60)
assert result == 80.0
def test_small_discount_is_quiet():
assert apply_discount(200.0, 10) == 180.0$ pytest -q test_discount.py
.. [100%]
2 passed in 0.01sblock तब पास होता है जब उसके अंदर कम से कम एक UserWarning (या उसकी subclass) निकले और उसका संदेश match से मेल खाए, जो एक regular expression है और टेक्स्ट में खोजा जाता है, ठीक pytest.raises की तरह। दोनों में से कोई भी हिस्सा टूटे तो साफ़ failure आता है। यहाँ कोड 50 पर शांत रहता है, और दूसरा टेस्ट गलत शब्द ढूँढता है:
import pytest
from pricing import apply_discount
def test_fifty_percent_warns():
with pytest.warns(UserWarning):
apply_discount(200.0, 50)
def test_wrong_message():
with pytest.warns(UserWarning, match="too large"):
apply_discount(200.0, 60)$ pytest -q test_discount_bad.py
FF [100%]
=================================== FAILURES ===================================
___________________________ test_fifty_percent_warns ___________________________
def test_fifty_percent_warns():
> with pytest.warns(UserWarning):
E Failed: DID NOT WARN. No warnings of type (<class 'UserWarning'>,) were emitted.
E Emitted warnings: [].
test_discount_bad.py:7: Failed
______________________________ test_wrong_message ______________________________
def test_wrong_message():
> with pytest.warns(UserWarning, match="too large"):
E Failed: Regex pattern did not match any of the 1 warnings emitted.
E Regex: 'too large'
E Emitted warnings: [UserWarning('discount of 60% looks unusually large')].
test_discount_bad.py:12: Failed
=============================== warnings summary ===============================
test_discount_bad.py::test_wrong_message
/home/you/wn/pricing.py:19: UserWarning: discount of 60% looks unusually large
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED test_discount_bad.py::test_fifty_percent_warns - Failed: DID NOT WARN....
FAILED test_discount_bad.py::test_wrong_message - Failed: Regex pattern did n...
2 failed, 1 warning in 0.01sदोनों रिपोर्ट्स उन warnings की सूची देती हैं जो सच में निकलीं, और टेस्ट ठीक करने के लिए आमतौर पर इतना ही चाहिए। जो warning match नहीं हुई, वह निगली भी नहीं जाती; वह आगे warnings summary में पहुँच जाती है।
हमेशा match क्यों दें। अकेला pytest.warns(UserWarning) कमज़ोर रूप है: block में कहीं से भी आई कोई भी UserWarning इसे संतुष्ट कर देती है — उस library की भी, जिसे आप संयोग से किसी असंबंधित कारण से call करते हैं। जिस warning की आपको परवाह है, उसके चले जाने के बाद भी टेस्ट पास होता रह सकता है। match किया गया वाक्यांश टेस्ट को इसी warning से बाँध देता है।
कितनी warnings निकलीं, या उन्होंने क्या कहा, यह जाँचने के लिए as से रिकॉर्ड रखें:
import pytest
from pricing import apply_discount
def test_each_large_discount_warns_once():
with pytest.warns(UserWarning) as record:
apply_discount(100.0, 60)
apply_discount(100.0, 10)
apply_discount(100.0, 75)
assert len(record) == 2
print(record[0].message)
print(record[1].category.__name__, record[1].lineno)$ pytest -q -s test_record.py
discount of 60% looks unusually large
UserWarning 19
.
1 passed in 0.01srecord block में पकड़ी गई हर warning की सूची है — दो, क्योंकि 10% वाली call शांत है। हर entry में .message, .category, .filename और .lineno होते हैं।
pytest.deprecated_call — आपके users से एक वादा
किसी फ़ंक्शन को deprecate करना एक वादा है: "यह अब भी काम करता है, और आपको बता दिया गया है"। pytest.deprecated_call() दोनों हिस्से जाँचता है:
import pytest
from pricing import total_with_vat
def test_old_name_still_works():
with pytest.deprecated_call():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q test_old_api.py
. [100%]
1 passed in 0.01sयह pytest.warns ही है जिसमें categories पहले से भरी हुई हैं — DeprecationWarning, PendingDeprecationWarning और FutureWarning — और यह भी match लेता है। warnings summary वाली entry ग़ायब हो गई: warning अपेक्षित थी, इसलिए वह consume हो गई।
filterwarnings — warnings को अस्वीकार करना
अपेक्षित warnings अब टेस्ट बन चुकी हैं। बाकी या तो शोर हैं या शुरुआती चेतावनियाँ, और CI के लिए अपनाने लायक आदत यह है: हर अनपेक्षित warning एक error है। एक लाइन:
[tool.pytest.ini_options]
filterwarnings = [
"error",
]यहाँ एक suite है जिसमें एक साफ़ टेस्ट है, एक जो अब भी deprecated नाम call करता है, और एक जो ऐसी library का currency.rate() call करता है जिसे आप बदल नहीं सकते, और जो अपनी DeprecationWarning देती है:
from currency import rate
from pricing import line_total, total_with_vat
def test_line_total():
assert line_total(100.0, 1) == 115.0
def test_old_name():
assert total_with_vat(100.0, 1) == 115.0
def test_rate():
assert rate("USD") == 1.0$ pytest -q --tb=short
.FF [100%]
=================================== FAILURES ===================================
________________________________ test_old_name _________________________________
test_shop.py:10: in test_old_name
assert total_with_vat(100.0, 1) == 115.0
^^^^^^^^^^^^^^^^^^^^^^^^
pricing.py:9: in total_with_vat
warnings.warn(
E DeprecationWarning: total_with_vat() is deprecated; use line_total()
__________________________________ test_rate ___________________________________
test_shop.py:14: in test_rate
assert rate("USD") == 1.0
^^^^^^^^^^^
currency.py:6: in rate
warnings.warn("rate() will need an API key from v3", DeprecationWarning)
E DeprecationWarning: rate() will need an API key from v3
=========================== short test summary info ============================
FAILED test_shop.py::test_old_name - DeprecationWarning: total_with_vat() is ...
FAILED test_shop.py::test_rate - DeprecationWarning: rate() will need an API ...
2 failed, 1 passed in 0.01sहर warning अब एक exception के रूप में उठती है, उस लाइन तक के traceback के साथ जिसने उसे पैदा किया। हर failure पर अब एक फ़ैसला होता है, सब पर एक साथ चुप्पी नहीं।
गलत तरीका है "ignore::DeprecationWarning": एक लाइन, सब कुछ हरा, और library के साथ-साथ आपका अपना deprecation भी छिप गया। सही तरीका है हर छूट को संकरा करना। filter का आकार action:message:category:module होता है — Python के अपने -W option जैसा ही — जहाँ message warning के टेक्स्ट की शुरुआत से मेल खाता है और module वह module है जिसके नाम warning दर्ज होती है:
[tool.pytest.ini_options]
filterwarnings = [
"error",
"ignore::DeprecationWarning:currency",
]क्रम मायने रखता है: बाद वाली लाइनों को प्राथमिकता मिलती है, इसलिए error पहले आता है और छूटें उसके बाद। library की warning अब केवल तब अनदेखी होती है जब वह currency से आए। किसी एक टेस्ट के लिए वही filter mark के रूप में लगाया जा सकता है — यहाँ test_old_name पर, फ़ाइल का बाकी हिस्सा जस का तस:
import pytest
from pricing import total_with_vat
@pytest.mark.filterwarnings("ignore:total_with_vat:DeprecationWarning")
def test_old_name():
assert total_with_vat(100.0, 1) == 115.0$ pytest -q
... [100%]
3 passed in 0.01sयह mark warning को केवल अनुमति देता है। अगर आपका मतलब है "यह warning ज़रूर देनी चाहिए", तो pytest.deprecated_call() बेहतर विकल्प है, क्योंकि warning ग़ायब होने पर वह fail भी होता है। जिन warnings को आप सहन करते हैं उनके लिए mark इस्तेमाल करें, और जिनका आप वादा करते हैं उनके लिए warns।
यही filters किसी एक run के लिए -W से भी दिए जा सकते हैं, जो किसी नीति को लिखकर तय करने से पहले आज़माने के लिए सुविधाजनक है। पहले वाली test_old_api.py पर, बिना किसी config के:
$ pytest -q -W error::DeprecationWarning --tb=no -ra
F [100%]
=========================== short test summary info ============================
FAILED test_old_api.py::test_old_name_still_works - DeprecationWarning: total...
1 failed in 0.01sजब स्रोत आपस में असहमत हों, तो सबसे विशिष्ट जीतता है: टेस्ट पर लगा mark -W से ऊपर है, और -W config फ़ाइल से ऊपर। तीन टेस्ट वाले suite पर, -W config के currency वाले ignore को override कर देता है, जबकि mark वाला टेस्ट अपनी छूट बनाए रखता है:
$ pytest -q -W error::DeprecationWarning --tb=no -ra
..F [100%]
=========================== short test summary info ============================
FAILED test_shop.py::test_rate - DeprecationWarning: rate() will need an API ...
1 failed, 2 passed in 0.01serror से आपको एक और चीज़ मिलती है। योजना की सीमा वाली पंक्ति, "50 पर कोई warning नहीं", को फिर से देखें। config में error होने पर, apply_discount(200.0, 50) का एक सादा टेस्ट इसे पहले से ही जाँचता है: अगर वह call कभी warning देती, तो warning एक exception बन जाती और टेस्ट fail हो जाता। शांत cases के लिए किसी ख़ास कोड की ज़रूरत नहीं।
पूर्ण उदाहरण
shop प्रोजेक्ट, हर नियम एक ही जगह। pyproject.toml:
[project]
name = "shop"
version = "0.1.0"
[tool.pytest.ini_options]
minversion = "9.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
xfail_strict = true
filterwarnings = [
"error",
]
log_cli_level = "INFO"src/shop/pricing.py:
import logging
import warnings
log = logging.getLogger(__name__)
def line_total(price, quantity):
if quantity < 1:
raise ValueError("quantity must be at least 1")
return round(price * quantity * 1.15, 2)
def total_with_vat(price, quantity):
warnings.warn(
"total_with_vat() is deprecated; use line_total()",
DeprecationWarning,
stacklevel=2,
)
return line_total(price, quantity)
def apply_discount(total, percent):
if percent > 50:
warnings.warn(f"discount of {percent}% looks unusually large", UserWarning)
log.info("discount %d%% on %.2f", percent, total)
return round(total * (100 - percent) / 100, 2)$ PYTHONPATH=src python -c "from shop.pricing import apply_discount; print(apply_discount(200.0, 10))"
180.0tests/test_pricing.py — अध्याय की शुरुआत वाली योजना, पंक्ति-दर-पंक्ति:
import pytest
from shop.pricing import apply_discount, line_total, total_with_vat
def test_line_total():
assert line_total(100.0, 1) == 115.0
def test_old_name_is_deprecated():
with pytest.deprecated_call(match="use line_total"):
assert total_with_vat(100.0, 1) == 115.0
def test_large_discount_warns():
with pytest.warns(UserWarning, match="unusually large") as record:
assert apply_discount(200.0, 60) == 80.0
assert len(record) == 1
def test_fifty_percent_is_quiet():
assert apply_discount(200.0, 50) == 100.0
@pytest.mark.slow
def test_many_lines():
total = sum(line_total(1.0, 1) for _ in range(10_000))
assert round(total, 2) == 11500.0
@pytest.mark.xfail(reason="bug #41: zero quantity should be free, not an error")
def test_zero_quantity():
assert line_total(15.0, 0) == 0.0$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/full
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_pricing.py .....x [100%]
=========================== short test summary info ============================
XFAIL tests/test_pricing.py::test_zero_quantity - bug #41: zero quantity should be free, not an error
========================= 5 passed, 1 xfailed in 0.01s =========================
$ pytest -m "not slow" -o log_cli=true
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/full
configfile: pyproject.toml
testpaths: tests
collected 6 items / 1 deselected / 5 selected
tests/test_pricing.py::test_line_total PASSED [ 20%]
tests/test_pricing.py::test_old_name_is_deprecated PASSED [ 40%]
tests/test_pricing.py::test_large_discount_warns
-------------------------------- live log call ---------------------------------
INFO shop.pricing:pricing.py:25 discount 60% on 200.00
PASSED [ 60%]
tests/test_pricing.py::test_fifty_percent_is_quiet
-------------------------------- live log call ---------------------------------
INFO shop.pricing:pricing.py:25 discount 50% on 200.00
PASSED [ 80%]
tests/test_pricing.py::test_zero_quantity XFAIL (bug #41: zero quant...) [100%]
=========================== short test summary info ============================
XFAIL tests/test_pricing.py::test_zero_quantity - bug #41: zero quantity should be free, not an error
================== 4 passed, 1 deselected, 1 xfailed in 0.01s ==================यहाँ तीन बातें ध्यान देने योग्य हैं।
पहली, दोनों में से किसी भी command में कोई प्रोजेक्ट नियम नहीं है। नियम — टेस्ट कहाँ हैं, क्या import हो सकता है, strict markers, strict xfail, warnings को error मानना — सब फ़ाइल में हैं, इसलिए जो साथी सिर्फ़ pytest टाइप करता है उसे ठीक पहला run मिलता है, और CI को भी।
दूसरी, कोई warnings summary नहीं है। कोड जो दोनों warnings देता है वे अपेक्षित हैं, इसलिए warns और deprecated_call ने उन्हें consume कर लिया; कोई भी दूसरी warning किसी टेस्ट को fail कर देती। साफ़ run अब सचमुच साफ़ है।
तीसरी, test_fifty_percent_is_quiet में warning से जुड़ा कोई कोड नहीं है, फिर भी यह सीमा की रखवाली करता है: error के तहत, 50% पर कोई warning इसे fail कर देती।
जब यह काम न करे
collection के दौरान ModuleNotFoundError: No module named 'shop' कोड टेस्ट्स से import नहीं हो पा रहा। config में pythonpath = ["src"] जोड़ें, और header की configfile: लाइन देखकर पक्का करें कि वही फ़ाइल पढ़ी जा रही है।
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!) दो config फ़ाइलें हैं, और जिसे आपने बदला वह हार गई। कुछ भी merge नहीं होता। एक को हटा दें।
'slow' not found in markers configuration option --strict-markers (या strict = true) चालू है, और marker या तो register नहीं है या उसकी spelling गलत है। उसे markers में register करें, या spelling ठीक करें।
PytestConfigWarning: Unknown config option: testpath गलत लिखी setting — testpaths की जगह testpath। pytest केवल warning देता है, इसलिए setting चुपचाप कुछ नहीं करती। strict_config = true (या strict = true) के साथ यह ERROR: Unknown config option: testpath बन जाती है और run रुक जाता है, जो आप चाहते हैं।
ERROR: ... Cannot use both [tool.pytest] (native TOML types) and [tool.pytest.ini_options] (string-based INI format) simultaneously. pyproject.toml में दोनों tables हैं। सब कुछ एक में ले जाएँ।
TypeError: ... config option 'addopts' expects a list for type 'args', got str: '-ra --strict-markers' native [tool.pytest] table में addopts एक list है: addopts = ["-ra", "--strict-markers"]। string वाला रूप [tool.pytest.ini_options] का है।
ERROR: ... 'minversion' requires pytest-10.0, actual pytest-9.1.1' install किया हुआ pytest प्रोजेक्ट की ज़रूरत से पुराना है। virtual environment के अंदर उसे upgrade करें।
Failed: DID NOT WARN. No warnings of type (<class 'UserWarning'>,) were emitted. block में उस category की कोई warning नहीं निकली। नीचे Emitted warnings: पढ़ें — अक्सर कोड किसी दूसरी category से warning देता है, या इस इनपुट पर देता ही नहीं।
Failed: Regex pattern did not match any of the 1 warnings emitted. category सही, टेक्स्ट गलत। match एक regular expression है, इसलिए (, . और ? को escape करना पड़ता है — या टेक्स्ट को re.escape() में लपेट दें।
AttributeError: module 'builtins' has no attribute 'DeprecatedWarning'. Did you mean: 'DeprecationWarning'? किसी filterwarnings लाइन की category में typo है। pytest शुरू होने से मना कर देता है और ठीक ऊपर वह लाइन प्रिंट करता है जिसे वह parse नहीं कर पाया।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
इसे pytest -q -s से चलाया गया। टेस्ट कौन-सी संख्या प्रिंट करेगा?
import warnings
import pytest
def check(n):
if n < 0:
warnings.warn("negative value", UserWarning)
return abs(n)
def test_record():
with pytest.warns(UserWarning) as record:
check(-1)
check(2)
check(-3)
print(len(record))- A3
- B1
- C2
- D0
प्रोजेक्ट रूट में pytest.ini और [tool.pytest.ini_options] वाली pyproject.toml दोनों हैं। pytest चलाने पर क्या होता है?
- Aदोनों फ़ाइलें मिला दी जाती हैं; कमी `pyproject.toml` पूरी करती है
- B`pytest.ini` इस्तेमाल होती है, `pyproject.toml` की सेटिंग्स अनदेखी होती हैं, और हेडर इसकी चेतावनी देता है
- C`pyproject.toml` जीतती है, क्योंकि वह नया मानक है
- Dदो कॉन्फ़िग फ़ाइलें होने पर pytest एरर देकर रुक जाता है
कॉन्फ़िग में addopts = -ra --strict-markers है और markers में केवल slow रजिस्टर है। pytest -m "not slow" चलाने पर क्या होगा?
import pytest
@pytest.mark.slwo
def test_every_day_of_a_year():
fees = [day * 5 for day in range(366)]
assert fees[-1] == 1825- Aटेस्ट इरादे के मुताबिक deselect हो जाता है
- Bटेस्ट चलता है, सारांश में एक चेतावनी के साथ
- Cटेस्ट skip हो जाता है
- Dकलेक्शन रुक जाता है: 'slwo' not found in `markers` configuration option
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
library प्रोजेक्ट देर से लौटाने पर शुल्क (late fee) लेता है। src/library/fees.py:
import warnings
PER_DAY = 5
def late_fee(days):
if days < 0:
raise ValueError("days cannot be negative")
if days > 60:
warnings.warn(f"{days} days late looks like a lost book", UserWarning)
return days * PER_DAY
def fine(days):
warnings.warn(
"fine() is deprecated; use late_fee()",
DeprecationWarning,
stacklevel=2,
)
return late_fee(days)$ PYTHONPATH=src python -c "from library.fees import late_fee; print(late_fee(4))"
20[tool.pytest.ini_options]table ऐसे लिखें कि सादाpytest, root से याtests/के अंदर से,libraryको import करे, केवलtests/को collect करे,-rasummaries प्रिंट करे, अनजान markers को अस्वीकार करे, किसी अनपेक्षितxfailके पास होने पर fail हो और हर अनपेक्षित warning को error में बदल दे। एकslowmarker register करें।- पहले टेस्ट योजना लिखें — केस, इनपुट, अपेक्षित — जिसमें समय पर वापसी, 60 दिन की सीमा, 61 दिन, एक ऋणात्मक संख्या और deprecated
fine()शामिल हों। - योजना से
tests/test_fees.pyलिखें, जिसमें एकslowटेस्ट हो जो साल के हर दिन से गुज़रे। pytestचलाएँ, फिरpytest -m "not slow", फिरcd tests && pytest।
फिर दो प्रयोग, हर एक का नतीजा पहले से अनुमान लगाकर: deprecated_call वाले टेस्ट की जगह fine(3) की एक सादी call रखें, और slow को slwo लिख दें।
समाधान
योजना:
| केस | इनपुट | अपेक्षित | | --- | --- | --- | | समय पर लौटाई | late_fee(0) | 0, कोई warning नहीं | | सीमा, अब भी शांत | late_fee(60) | 300, कोई warning नहीं | | सीमा के ठीक पार | late_fee(61) | 305 और एक UserWarning जिसमें "lost book" हो | | अमान्य इनपुट | late_fee(-1) | ValueError जिसमें "negative" हो | | deprecated नाम | fine(3) | 15 और एक DeprecationWarning जिसमें late_fee हो | | पूरा एक साल (slow) | late_fee(0) … late_fee(365) | आख़िरी शुल्क 1825, warnings अपेक्षित |
pyproject.toml:
[project]
name = "library"
version = "0.1.0"
[tool.pytest.ini_options]
minversion = "9.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra --strict-markers"
markers = [
"slow: takes more than a second; deselect with -m 'not slow'",
]
xfail_strict = true
filterwarnings = [
"error",
]tests/test_fees.py:
import pytest
from library.fees import fine, late_fee
def test_returned_on_time():
assert late_fee(0) == 0
def test_sixty_days_is_quiet():
# filterwarnings = error: any warning here would fail this test
assert late_fee(60) == 300
def test_sixty_one_days_warns():
with pytest.warns(UserWarning, match="lost book") as record:
assert late_fee(61) == 305
assert len(record) == 1
def test_negative_days_rejected():
with pytest.raises(ValueError, match="negative"):
late_fee(-1)
def test_fine_is_deprecated():
with pytest.deprecated_call(match="use late_fee"):
assert fine(3) == 15
@pytest.mark.slow
def test_every_day_of_a_year():
with pytest.warns(UserWarning):
fees = [late_fee(day) for day in range(366)]
assert fees[-1] == 1825$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/library
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_fees.py ...... [100%]
============================== 6 passed in 0.01s ===============================
$ pytest -m "not slow" -q
..... [100%]
5 passed, 1 deselected in 0.01s
$ cd tests && pytest -q
...... [100%]
6 passed in 0.01sदोनों प्रयोग:
$ pytest -q # test_fine_still_works calls fine(3) with no deprecated_call
=========================== short test summary info ============================
FAILED tests/test_fees.py::test_fine_still_works - DeprecationWarning: fine()...
1 failed, 5 passed in 0.01s
$ pytest -q # @pytest.mark.slwo
ERROR tests/test_fees.py - Failed: 'slwo' not found in `markers` configuratio...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sहर फ़ैसले की वजह:
pythonpathऔरtestpathsफ़ाइल में, command line पर नहीं। इसी वजह सेcd tests && pytestभी वही छह पास देता है: दोनों rootdir के सापेक्ष हैं, जिसे pytest ऊपरpyproject.tomlतक चलकर ढूँढता है।addoptsमें केवल नियम।-raऔर--strict-markersऐसी नीतियाँ हैं जो सब साझा करते हैं;-vया-xनिजी आदतें हैं और आपकी अपनी command line पर ही रहती हैं।filterwarnings = ["error"]बिना किसी छूट के। प्रोजेक्ट में अभी कोई third-party warning नहीं है, इसलिए माफ़ करने को कुछ नहीं है। जिस दिन कोई आए, एक संकरीignore::...:moduleलाइन जोड़ें, कभी भी एक सामान्यignoreनहीं।- 60 दिन वाले टेस्ट में warning का कोई कोड नहीं।
errorके तहत, 60 दिन पर कोई भी warning इसे fail कर देती, इसलिए config ही सीमा की रखवाली करता है। - किसी एक संदेश को निशाना बनाने वाली हर जाँच पर
match। "lost book" और "use late_fee" वे शब्द हैं जिन पर पढ़ने वाला कार्रवाई करता है; बाकी को खुलकर बदला जा सकता है। साल भर वाला टेस्ट जानबूझकरmatchछोड़ता है: वह शुल्कों के बारे में है, और उसे बस इतना कहना है कि वहाँ warnings अपेक्षित हैं। - 61 दिन वाले टेस्ट में
record। यह साबित करता है कि एक call एक बार warning देती है, हर अंदरूनी कदम पर नहीं। - प्रयोग।
deprecated_callके बिना deprecation अनपेक्षित है, इसलिएerrorउसे failure में बदल देता है — ठीक वही जो CI को करना चाहिए जब कोई पुराना API call करे। गलत लिखा marker collection को रोक देता है क्योंकि--strict-markersaddoptsमें है। (यहाँerrorfilter भी इसे पकड़ लेता, क्योंकि अनजान marker एकPytestUnknownMarkWarningदेता है — लेकिन marker वाला नियम warning नीति पर निर्भर नहीं होना चाहिए, इसलिए दोनों रहते हैं।)
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz