pytestplugin.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. # testing/plugin/pytestplugin.py
  2. # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. from __future__ import annotations
  9. import argparse
  10. import collections
  11. from functools import update_wrapper
  12. import inspect
  13. import itertools
  14. import operator
  15. import os
  16. import re
  17. import sys
  18. from typing import TYPE_CHECKING
  19. import uuid
  20. import pytest
  21. try:
  22. # installed by bootstrap.py
  23. if not TYPE_CHECKING:
  24. import sqla_plugin_base as plugin_base
  25. except ImportError:
  26. # assume we're a package, use traditional import
  27. from . import plugin_base
  28. def pytest_addoption(parser):
  29. group = parser.getgroup("sqlalchemy")
  30. def make_option(name, **kw):
  31. callback_ = kw.pop("callback", None)
  32. if callback_:
  33. class CallableAction(argparse.Action):
  34. def __call__(
  35. self, parser, namespace, values, option_string=None
  36. ):
  37. callback_(option_string, values, parser)
  38. kw["action"] = CallableAction
  39. zeroarg_callback = kw.pop("zeroarg_callback", None)
  40. if zeroarg_callback:
  41. class CallableAction(argparse.Action):
  42. def __init__(
  43. self,
  44. option_strings,
  45. dest,
  46. default=False,
  47. required=False,
  48. help=None, # noqa
  49. ):
  50. super().__init__(
  51. option_strings=option_strings,
  52. dest=dest,
  53. nargs=0,
  54. const=True,
  55. default=default,
  56. required=required,
  57. help=help,
  58. )
  59. def __call__(
  60. self, parser, namespace, values, option_string=None
  61. ):
  62. zeroarg_callback(option_string, values, parser)
  63. kw["action"] = CallableAction
  64. group.addoption(name, **kw)
  65. plugin_base.setup_options(make_option)
  66. def pytest_configure(config: pytest.Config):
  67. plugin_base.read_config(config.rootpath)
  68. if plugin_base.exclude_tags or plugin_base.include_tags:
  69. new_expr = " and ".join(
  70. list(plugin_base.include_tags)
  71. + [f"not {tag}" for tag in plugin_base.exclude_tags]
  72. )
  73. if config.option.markexpr:
  74. config.option.markexpr += f" and {new_expr}"
  75. else:
  76. config.option.markexpr = new_expr
  77. if config.pluginmanager.hasplugin("xdist"):
  78. config.pluginmanager.register(XDistHooks())
  79. if hasattr(config, "workerinput"):
  80. plugin_base.restore_important_follower_config(config.workerinput)
  81. plugin_base.configure_follower(config.workerinput["follower_ident"])
  82. else:
  83. if config.option.write_idents and os.path.exists(
  84. config.option.write_idents
  85. ):
  86. os.remove(config.option.write_idents)
  87. plugin_base.pre_begin(config.option)
  88. plugin_base.set_coverage_flag(
  89. bool(getattr(config.option, "cov_source", False))
  90. )
  91. plugin_base.set_fixture_functions(PytestFixtureFunctions)
  92. if config.option.dump_pyannotate:
  93. global DUMP_PYANNOTATE
  94. DUMP_PYANNOTATE = True
  95. DUMP_PYANNOTATE = False
  96. @pytest.fixture(autouse=True)
  97. def collect_types_fixture():
  98. if DUMP_PYANNOTATE:
  99. from pyannotate_runtime import collect_types
  100. collect_types.start()
  101. yield
  102. if DUMP_PYANNOTATE:
  103. collect_types.stop()
  104. def _log_sqlalchemy_info(session):
  105. import sqlalchemy
  106. from sqlalchemy import __version__
  107. from sqlalchemy.util import has_compiled_ext
  108. from sqlalchemy.util._has_cy import _CYEXTENSION_MSG
  109. greet = "sqlalchemy installation"
  110. site = "no user site" if sys.flags.no_user_site else "user site loaded"
  111. msgs = [
  112. f"SQLAlchemy {__version__} ({site})",
  113. f"Path: {sqlalchemy.__file__}",
  114. ]
  115. if has_compiled_ext():
  116. from sqlalchemy.cyextension import util
  117. msgs.append(f"compiled extension enabled, e.g. {util.__file__} ")
  118. else:
  119. msgs.append(f"compiled extension not enabled; {_CYEXTENSION_MSG}")
  120. pm = session.config.pluginmanager.get_plugin("terminalreporter")
  121. if pm:
  122. pm.write_sep("=", greet)
  123. for m in msgs:
  124. pm.write_line(m)
  125. else:
  126. # fancy pants reporter not found, fallback to plain print
  127. print("=" * 25, greet, "=" * 25)
  128. for m in msgs:
  129. print(m)
  130. def pytest_sessionstart(session):
  131. from sqlalchemy.testing import asyncio
  132. _log_sqlalchemy_info(session)
  133. asyncio._assume_async(plugin_base.post_begin)
  134. def pytest_sessionfinish(session):
  135. from sqlalchemy.testing import asyncio
  136. asyncio._maybe_async_provisioning(plugin_base.final_process_cleanup)
  137. if session.config.option.dump_pyannotate:
  138. from pyannotate_runtime import collect_types
  139. collect_types.dump_stats(session.config.option.dump_pyannotate)
  140. def pytest_unconfigure(config):
  141. from sqlalchemy.testing import asyncio
  142. asyncio._shutdown()
  143. def pytest_collection_finish(session):
  144. if session.config.option.dump_pyannotate:
  145. from pyannotate_runtime import collect_types
  146. lib_sqlalchemy = os.path.abspath("lib/sqlalchemy")
  147. def _filter(filename):
  148. filename = os.path.normpath(os.path.abspath(filename))
  149. if "lib/sqlalchemy" not in os.path.commonpath(
  150. [filename, lib_sqlalchemy]
  151. ):
  152. return None
  153. if "testing" in filename:
  154. return None
  155. return filename
  156. collect_types.init_types_collection(filter_filename=_filter)
  157. class XDistHooks:
  158. def pytest_configure_node(self, node):
  159. from sqlalchemy.testing import provision
  160. from sqlalchemy.testing import asyncio
  161. # the master for each node fills workerinput dictionary
  162. # which pytest-xdist will transfer to the subprocess
  163. plugin_base.memoize_important_follower_config(node.workerinput)
  164. node.workerinput["follower_ident"] = "test_%s" % uuid.uuid4().hex[0:12]
  165. asyncio._maybe_async_provisioning(
  166. provision.create_follower_db, node.workerinput["follower_ident"]
  167. )
  168. def pytest_testnodedown(self, node, error):
  169. from sqlalchemy.testing import provision
  170. from sqlalchemy.testing import asyncio
  171. asyncio._maybe_async_provisioning(
  172. provision.drop_follower_db, node.workerinput["follower_ident"]
  173. )
  174. def pytest_collection_modifyitems(session, config, items):
  175. # look for all those classes that specify __backend__ and
  176. # expand them out into per-database test cases.
  177. # this is much easier to do within pytest_pycollect_makeitem, however
  178. # pytest is iterating through cls.__dict__ as makeitem is
  179. # called which causes a "dictionary changed size" error on py3k.
  180. # I'd submit a pullreq for them to turn it into a list first, but
  181. # it's to suit the rather odd use case here which is that we are adding
  182. # new classes to a module on the fly.
  183. from sqlalchemy.testing import asyncio
  184. rebuilt_items = collections.defaultdict(
  185. lambda: collections.defaultdict(list)
  186. )
  187. items[:] = [
  188. item
  189. for item in items
  190. if item.getparent(pytest.Class) is not None
  191. and not item.getparent(pytest.Class).name.startswith("_")
  192. ]
  193. test_classes = {item.getparent(pytest.Class) for item in items}
  194. def collect(element):
  195. for inst_or_fn in element.collect():
  196. if isinstance(inst_or_fn, pytest.Collector):
  197. yield from collect(inst_or_fn)
  198. else:
  199. yield inst_or_fn
  200. def setup_test_classes():
  201. for test_class in test_classes:
  202. # transfer legacy __backend__ and __sparse_backend__ symbols
  203. # to be markers
  204. if getattr(test_class.cls, "__backend__", False) or getattr(
  205. test_class.cls, "__only_on__", False
  206. ):
  207. add_markers = {"backend"}
  208. elif getattr(test_class.cls, "__sparse_backend__", False):
  209. add_markers = {"sparse_backend", "backend"}
  210. elif getattr(test_class.cls, "__sparse_driver_backend__", False):
  211. add_markers = {"sparse_driver_backend", "backend"}
  212. else:
  213. add_markers = frozenset()
  214. existing_markers = {
  215. mark.name for mark in test_class.iter_markers()
  216. }
  217. add_markers = add_markers - existing_markers
  218. all_markers = existing_markers.union(add_markers)
  219. for marker in add_markers:
  220. test_class.add_marker(marker)
  221. sub_tests = list(
  222. plugin_base.generate_sub_tests(
  223. test_class.cls, test_class.module, all_markers
  224. )
  225. )
  226. if not sub_tests:
  227. rebuilt_items[test_class.cls]
  228. for sub_cls in sub_tests:
  229. if sub_cls is not test_class.cls:
  230. per_cls_dict = rebuilt_items[test_class.cls]
  231. module = test_class.getparent(pytest.Module)
  232. new_cls = pytest.Class.from_parent(
  233. name=sub_cls.__name__, parent=module
  234. )
  235. for marker in add_markers:
  236. new_cls.add_marker(marker)
  237. for fn in collect(new_cls):
  238. per_cls_dict[fn.name].append(fn)
  239. # class requirements will sometimes need to access the DB to check
  240. # capabilities, so need to do this for async
  241. asyncio._maybe_async_provisioning(setup_test_classes)
  242. newitems = []
  243. for item in items:
  244. cls_ = item.cls
  245. if cls_ in rebuilt_items:
  246. newitems.extend(rebuilt_items[cls_][item.name])
  247. else:
  248. newitems.append(item)
  249. # seems like the functions attached to a test class aren't sorted already?
  250. # is that true and why's that? (when using unittest, they're sorted)
  251. items[:] = sorted(
  252. newitems,
  253. key=lambda item: (
  254. item.getparent(pytest.Module).name,
  255. item.getparent(pytest.Class).name,
  256. item.name,
  257. ),
  258. )
  259. def pytest_pycollect_makeitem(collector, name, obj):
  260. if inspect.isclass(obj) and plugin_base.want_class(name, obj):
  261. from sqlalchemy.testing import config
  262. if config.any_async:
  263. obj = _apply_maybe_async(obj)
  264. return [
  265. pytest.Class.from_parent(
  266. name=parametrize_cls.__name__, parent=collector
  267. )
  268. for parametrize_cls in _parametrize_cls(collector.module, obj)
  269. ]
  270. elif (
  271. inspect.isfunction(obj)
  272. and collector.cls is not None
  273. and plugin_base.want_method(collector.cls, obj)
  274. ):
  275. # None means, fall back to default logic, which includes
  276. # method-level parametrize
  277. return None
  278. else:
  279. # empty list means skip this item
  280. return []
  281. def _is_wrapped_coroutine_function(fn):
  282. while hasattr(fn, "__wrapped__"):
  283. fn = fn.__wrapped__
  284. return inspect.iscoroutinefunction(fn)
  285. def _apply_maybe_async(obj, recurse=True):
  286. from sqlalchemy.testing import asyncio
  287. for name, value in vars(obj).items():
  288. if (
  289. (callable(value) or isinstance(value, classmethod))
  290. and not getattr(value, "_maybe_async_applied", False)
  291. and (name.startswith("test_"))
  292. and not _is_wrapped_coroutine_function(value)
  293. ):
  294. is_classmethod = False
  295. if isinstance(value, classmethod):
  296. value = value.__func__
  297. is_classmethod = True
  298. @_pytest_fn_decorator
  299. def make_async(fn, *args, **kwargs):
  300. return asyncio._maybe_async(fn, *args, **kwargs)
  301. do_async = make_async(value)
  302. if is_classmethod:
  303. do_async = classmethod(do_async)
  304. do_async._maybe_async_applied = True
  305. setattr(obj, name, do_async)
  306. if recurse:
  307. for cls in obj.mro()[1:]:
  308. if cls != object:
  309. _apply_maybe_async(cls, False)
  310. return obj
  311. def _parametrize_cls(module, cls):
  312. """implement a class-based version of pytest parametrize."""
  313. if "_sa_parametrize" not in cls.__dict__:
  314. return [cls]
  315. _sa_parametrize = cls._sa_parametrize
  316. classes = []
  317. for full_param_set in itertools.product(
  318. *[params for argname, params in _sa_parametrize]
  319. ):
  320. cls_variables = {}
  321. for argname, param in zip(
  322. [_sa_param[0] for _sa_param in _sa_parametrize], full_param_set
  323. ):
  324. if not argname:
  325. raise TypeError("need argnames for class-based combinations")
  326. argname_split = re.split(r",\s*", argname)
  327. for arg, val in zip(argname_split, param.values):
  328. cls_variables[arg] = val
  329. parametrized_name = "_".join(
  330. re.sub(r"\W", "", token)
  331. for param in full_param_set
  332. for token in param.id.split("-")
  333. )
  334. name = "%s_%s" % (cls.__name__, parametrized_name)
  335. newcls = type.__new__(type, name, (cls,), cls_variables)
  336. setattr(module, name, newcls)
  337. classes.append(newcls)
  338. return classes
  339. _current_class = None
  340. _current_warning_context = None
  341. def pytest_runtest_setup(item):
  342. from sqlalchemy.testing import asyncio
  343. # pytest_runtest_setup runs *before* pytest fixtures with scope="class".
  344. # plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
  345. # for the whole class and has to run things that are across all current
  346. # databases, so we run this outside of the pytest fixture system altogether
  347. # and ensure asyncio greenlet if any engines are async
  348. global _current_class, _current_warning_context
  349. if isinstance(item, pytest.Function) and _current_class is None:
  350. asyncio._maybe_async_provisioning(
  351. plugin_base.start_test_class_outside_fixtures,
  352. item.cls,
  353. )
  354. _current_class = item.getparent(pytest.Class)
  355. if hasattr(_current_class.cls, "__warnings__"):
  356. import warnings
  357. _current_warning_context = warnings.catch_warnings()
  358. _current_warning_context.__enter__()
  359. for warning_message in _current_class.cls.__warnings__:
  360. warnings.filterwarnings("ignore", warning_message)
  361. @pytest.hookimpl(hookwrapper=True)
  362. def pytest_runtest_teardown(item, nextitem):
  363. # runs inside of pytest function fixture scope
  364. # after test function runs
  365. from sqlalchemy.testing import asyncio
  366. asyncio._maybe_async(plugin_base.after_test, item)
  367. yield
  368. # this is now after all the fixture teardown have run, the class can be
  369. # finalized. Since pytest v7 this finalizer can no longer be added in
  370. # pytest_runtest_setup since the class has not yet been setup at that
  371. # time.
  372. # See https://github.com/pytest-dev/pytest/issues/9343
  373. global _current_class, _current_report, _current_warning_context
  374. if _current_class is not None and (
  375. # last test or a new class
  376. nextitem is None
  377. or nextitem.getparent(pytest.Class) is not _current_class
  378. ):
  379. if _current_warning_context is not None:
  380. _current_warning_context.__exit__(None, None, None)
  381. _current_warning_context = None
  382. _current_class = None
  383. try:
  384. asyncio._maybe_async_provisioning(
  385. plugin_base.stop_test_class_outside_fixtures, item.cls
  386. )
  387. except Exception as e:
  388. # in case of an exception during teardown attach the original
  389. # error to the exception message, otherwise it will get lost
  390. if _current_report.failed:
  391. if not e.args:
  392. e.args = (
  393. "__Original test failure__:\n"
  394. + _current_report.longreprtext,
  395. )
  396. elif e.args[-1] and isinstance(e.args[-1], str):
  397. args = list(e.args)
  398. args[-1] += (
  399. "\n__Original test failure__:\n"
  400. + _current_report.longreprtext
  401. )
  402. e.args = tuple(args)
  403. else:
  404. e.args += (
  405. "__Original test failure__",
  406. _current_report.longreprtext,
  407. )
  408. raise
  409. finally:
  410. _current_report = None
  411. def pytest_runtest_call(item):
  412. # runs inside of pytest function fixture scope
  413. # before test function runs
  414. from sqlalchemy.testing import asyncio
  415. asyncio._maybe_async(
  416. plugin_base.before_test,
  417. item,
  418. item.module.__name__,
  419. item.cls,
  420. item.name,
  421. )
  422. _current_report = None
  423. def pytest_runtest_logreport(report):
  424. global _current_report
  425. if report.when == "call":
  426. _current_report = report
  427. @pytest.fixture(scope="class")
  428. def setup_class_methods(request):
  429. from sqlalchemy.testing import asyncio
  430. cls = request.cls
  431. if hasattr(cls, "setup_test_class"):
  432. asyncio._maybe_async(cls.setup_test_class)
  433. yield
  434. if hasattr(cls, "teardown_test_class"):
  435. asyncio._maybe_async(cls.teardown_test_class)
  436. asyncio._maybe_async(plugin_base.stop_test_class, cls)
  437. @pytest.fixture(scope="function")
  438. def setup_test_methods(request):
  439. from sqlalchemy.testing import asyncio
  440. # called for each test
  441. self = request.instance
  442. # before this fixture runs:
  443. # 1. function level "autouse" fixtures under py3k (examples: TablesTest
  444. # define tables / data, MappedTest define tables / mappers / data)
  445. # 2. was for p2k. no longer applies
  446. # 3. run outer xdist-style setup
  447. if hasattr(self, "setup_test"):
  448. asyncio._maybe_async(self.setup_test)
  449. # alembic test suite is using setUp and tearDown
  450. # xdist methods; support these in the test suite
  451. # for the near term
  452. if hasattr(self, "setUp"):
  453. asyncio._maybe_async(self.setUp)
  454. # inside the yield:
  455. # 4. function level fixtures defined on test functions themselves,
  456. # e.g. "connection", "metadata" run next
  457. # 5. pytest hook pytest_runtest_call then runs
  458. # 6. test itself runs
  459. yield
  460. # yield finishes:
  461. # 7. function level fixtures defined on test functions
  462. # themselves, e.g. "connection" rolls back the transaction, "metadata"
  463. # emits drop all
  464. # 8. pytest hook pytest_runtest_teardown hook runs, this is associated
  465. # with fixtures close all sessions, provisioning.stop_test_class(),
  466. # engines.testing_reaper -> ensure all connection pool connections
  467. # are returned, engines created by testing_engine that aren't the
  468. # config engine are disposed
  469. asyncio._maybe_async(plugin_base.after_test_fixtures, self)
  470. # 10. run xdist-style teardown
  471. if hasattr(self, "tearDown"):
  472. asyncio._maybe_async(self.tearDown)
  473. if hasattr(self, "teardown_test"):
  474. asyncio._maybe_async(self.teardown_test)
  475. # 11. was for p2k. no longer applies
  476. # 12. function level "autouse" fixtures under py3k (examples: TablesTest /
  477. # MappedTest delete table data, possibly drop tables and clear mappers
  478. # depending on the flags defined by the test class)
  479. def _pytest_fn_decorator(target):
  480. """Port of langhelpers.decorator with pytest-specific tricks."""
  481. from sqlalchemy.util.langhelpers import format_argspec_plus
  482. from sqlalchemy.util.compat import inspect_getfullargspec
  483. def _exec_code_in_env(code, env, fn_name):
  484. # note this is affected by "from __future__ import annotations" at
  485. # the top; exec'ed code will use non-evaluated annotations
  486. # which allows us to be more flexible with code rendering
  487. # in format_argpsec_plus()
  488. exec(code, env)
  489. return env[fn_name]
  490. def decorate(fn, add_positional_parameters=()):
  491. spec = inspect_getfullargspec(fn)
  492. if add_positional_parameters:
  493. spec.args.extend(add_positional_parameters)
  494. metadata = dict(
  495. __target_fn="__target_fn", __orig_fn="__orig_fn", name=fn.__name__
  496. )
  497. metadata.update(format_argspec_plus(spec, grouped=False))
  498. code = (
  499. """\
  500. def %(name)s%(grouped_args)s:
  501. return %(__target_fn)s(%(__orig_fn)s, %(apply_kw)s)
  502. """
  503. % metadata
  504. )
  505. decorated = _exec_code_in_env(
  506. code, {"__target_fn": target, "__orig_fn": fn}, fn.__name__
  507. )
  508. if not add_positional_parameters:
  509. decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__
  510. decorated.__wrapped__ = fn
  511. return update_wrapper(decorated, fn)
  512. else:
  513. # this is the pytest hacky part. don't do a full update wrapper
  514. # because pytest is really being sneaky about finding the args
  515. # for the wrapped function
  516. decorated.__module__ = fn.__module__
  517. decorated.__name__ = fn.__name__
  518. if hasattr(fn, "pytestmark"):
  519. decorated.pytestmark = fn.pytestmark
  520. return decorated
  521. return decorate
  522. class PytestFixtureFunctions(plugin_base.FixtureFunctions):
  523. def skip_test_exception(self, *arg, **kw):
  524. return pytest.skip.Exception(*arg, **kw)
  525. @property
  526. def add_to_marker(self):
  527. return pytest.mark
  528. def mark_base_test_class(self):
  529. return pytest.mark.usefixtures(
  530. "setup_class_methods",
  531. "setup_test_methods",
  532. )
  533. _combination_id_fns = {
  534. "i": lambda obj: obj,
  535. "r": repr,
  536. "s": str,
  537. "n": lambda obj: (
  538. obj.__name__ if hasattr(obj, "__name__") else type(obj).__name__
  539. ),
  540. }
  541. def combinations(self, *arg_sets, **kw):
  542. """Facade for pytest.mark.parametrize.
  543. Automatically derives argument names from the callable which in our
  544. case is always a method on a class with positional arguments.
  545. ids for parameter sets are derived using an optional template.
  546. """
  547. from sqlalchemy.testing import exclusions
  548. if len(arg_sets) == 1 and hasattr(arg_sets[0], "__next__"):
  549. arg_sets = list(arg_sets[0])
  550. argnames = kw.pop("argnames", None)
  551. def _filter_exclusions(args):
  552. result = []
  553. gathered_exclusions = []
  554. for a in args:
  555. if isinstance(a, exclusions.compound):
  556. gathered_exclusions.append(a)
  557. else:
  558. result.append(a)
  559. return result, gathered_exclusions
  560. id_ = kw.pop("id_", None)
  561. tobuild_pytest_params = []
  562. has_exclusions = False
  563. if id_:
  564. _combination_id_fns = self._combination_id_fns
  565. # because itemgetter is not consistent for one argument vs.
  566. # multiple, make it multiple in all cases and use a slice
  567. # to omit the first argument
  568. _arg_getter = operator.itemgetter(
  569. 0,
  570. *[
  571. idx
  572. for idx, char in enumerate(id_)
  573. if char in ("n", "r", "s", "a")
  574. ],
  575. )
  576. fns = [
  577. (operator.itemgetter(idx), _combination_id_fns[char])
  578. for idx, char in enumerate(id_)
  579. if char in _combination_id_fns
  580. ]
  581. for arg in arg_sets:
  582. if not isinstance(arg, tuple):
  583. arg = (arg,)
  584. fn_params, param_exclusions = _filter_exclusions(arg)
  585. parameters = _arg_getter(fn_params)[1:]
  586. if param_exclusions:
  587. has_exclusions = True
  588. tobuild_pytest_params.append(
  589. (
  590. parameters,
  591. param_exclusions,
  592. "-".join(
  593. comb_fn(getter(arg)) for getter, comb_fn in fns
  594. ),
  595. )
  596. )
  597. else:
  598. for arg in arg_sets:
  599. if not isinstance(arg, tuple):
  600. arg = (arg,)
  601. fn_params, param_exclusions = _filter_exclusions(arg)
  602. if param_exclusions:
  603. has_exclusions = True
  604. tobuild_pytest_params.append(
  605. (fn_params, param_exclusions, None)
  606. )
  607. pytest_params = []
  608. for parameters, param_exclusions, id_ in tobuild_pytest_params:
  609. if has_exclusions:
  610. parameters += (param_exclusions,)
  611. param = pytest.param(*parameters, id=id_)
  612. pytest_params.append(param)
  613. def decorate(fn):
  614. if inspect.isclass(fn):
  615. if has_exclusions:
  616. raise NotImplementedError(
  617. "exclusions not supported for class level combinations"
  618. )
  619. if "_sa_parametrize" not in fn.__dict__:
  620. fn._sa_parametrize = []
  621. fn._sa_parametrize.append((argnames, pytest_params))
  622. return fn
  623. else:
  624. _fn_argnames = inspect.getfullargspec(fn).args[1:]
  625. if argnames is None:
  626. _argnames = _fn_argnames
  627. else:
  628. _argnames = re.split(r", *", argnames)
  629. if has_exclusions:
  630. existing_exl = sum(
  631. 1 for n in _fn_argnames if n.startswith("_exclusions")
  632. )
  633. current_exclusion_name = f"_exclusions_{existing_exl}"
  634. _argnames += [current_exclusion_name]
  635. @_pytest_fn_decorator
  636. def check_exclusions(fn, *args, **kw):
  637. _exclusions = args[-1]
  638. if _exclusions:
  639. exlu = exclusions.compound().add(*_exclusions)
  640. fn = exlu(fn)
  641. return fn(*args[:-1], **kw)
  642. fn = check_exclusions(
  643. fn, add_positional_parameters=(current_exclusion_name,)
  644. )
  645. return pytest.mark.parametrize(_argnames, pytest_params)(fn)
  646. return decorate
  647. def param_ident(self, *parameters):
  648. ident = parameters[0]
  649. return pytest.param(*parameters[1:], id=ident)
  650. def fixture(self, *arg, **kw):
  651. from sqlalchemy.testing import config
  652. from sqlalchemy.testing import asyncio
  653. # wrapping pytest.fixture function. determine if
  654. # decorator was called as @fixture or @fixture().
  655. if len(arg) > 0 and callable(arg[0]):
  656. # was called as @fixture(), we have the function to wrap.
  657. fn = arg[0]
  658. arg = arg[1:]
  659. else:
  660. # was called as @fixture, don't have the function yet.
  661. fn = None
  662. # create a pytest.fixture marker. because the fn is not being
  663. # passed, this is always a pytest.FixtureFunctionMarker()
  664. # object (or whatever pytest is calling it when you read this)
  665. # that is waiting for a function.
  666. fixture = pytest.fixture(*arg, **kw)
  667. # now apply wrappers to the function, including fixture itself
  668. def wrap(fn):
  669. if config.any_async:
  670. fn = asyncio._maybe_async_wrapper(fn)
  671. # other wrappers may be added here
  672. # now apply FixtureFunctionMarker
  673. fn = fixture(fn)
  674. return fn
  675. if fn:
  676. return wrap(fn)
  677. else:
  678. return wrap
  679. def get_current_test_name(self):
  680. return os.environ.get("PYTEST_CURRENT_TEST")
  681. def async_test(self, fn):
  682. from sqlalchemy.testing import asyncio
  683. @_pytest_fn_decorator
  684. def decorate(fn, *args, **kwargs):
  685. asyncio._run_coroutine_function(fn, *args, **kwargs)
  686. return decorate(fn)