pytestplugin.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. # testing/plugin/pytestplugin.py
  2. # Copyright (C) 2005-2025 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"}
  210. else:
  211. add_markers = frozenset()
  212. existing_markers = {
  213. mark.name for mark in test_class.iter_markers()
  214. }
  215. add_markers = add_markers - existing_markers
  216. all_markers = existing_markers.union(add_markers)
  217. for marker in add_markers:
  218. test_class.add_marker(marker)
  219. for sub_cls in plugin_base.generate_sub_tests(
  220. test_class.cls, test_class.module, all_markers
  221. ):
  222. if sub_cls is not test_class.cls:
  223. per_cls_dict = rebuilt_items[test_class.cls]
  224. module = test_class.getparent(pytest.Module)
  225. new_cls = pytest.Class.from_parent(
  226. name=sub_cls.__name__, parent=module
  227. )
  228. for marker in add_markers:
  229. new_cls.add_marker(marker)
  230. for fn in collect(new_cls):
  231. per_cls_dict[fn.name].append(fn)
  232. # class requirements will sometimes need to access the DB to check
  233. # capabilities, so need to do this for async
  234. asyncio._maybe_async_provisioning(setup_test_classes)
  235. newitems = []
  236. for item in items:
  237. cls_ = item.cls
  238. if cls_ in rebuilt_items:
  239. newitems.extend(rebuilt_items[cls_][item.name])
  240. else:
  241. newitems.append(item)
  242. # seems like the functions attached to a test class aren't sorted already?
  243. # is that true and why's that? (when using unittest, they're sorted)
  244. items[:] = sorted(
  245. newitems,
  246. key=lambda item: (
  247. item.getparent(pytest.Module).name,
  248. item.getparent(pytest.Class).name,
  249. item.name,
  250. ),
  251. )
  252. def pytest_pycollect_makeitem(collector, name, obj):
  253. if inspect.isclass(obj) and plugin_base.want_class(name, obj):
  254. from sqlalchemy.testing import config
  255. if config.any_async:
  256. obj = _apply_maybe_async(obj)
  257. return [
  258. pytest.Class.from_parent(
  259. name=parametrize_cls.__name__, parent=collector
  260. )
  261. for parametrize_cls in _parametrize_cls(collector.module, obj)
  262. ]
  263. elif (
  264. inspect.isfunction(obj)
  265. and collector.cls is not None
  266. and plugin_base.want_method(collector.cls, obj)
  267. ):
  268. # None means, fall back to default logic, which includes
  269. # method-level parametrize
  270. return None
  271. else:
  272. # empty list means skip this item
  273. return []
  274. def _is_wrapped_coroutine_function(fn):
  275. while hasattr(fn, "__wrapped__"):
  276. fn = fn.__wrapped__
  277. return inspect.iscoroutinefunction(fn)
  278. def _apply_maybe_async(obj, recurse=True):
  279. from sqlalchemy.testing import asyncio
  280. for name, value in vars(obj).items():
  281. if (
  282. (callable(value) or isinstance(value, classmethod))
  283. and not getattr(value, "_maybe_async_applied", False)
  284. and (name.startswith("test_"))
  285. and not _is_wrapped_coroutine_function(value)
  286. ):
  287. is_classmethod = False
  288. if isinstance(value, classmethod):
  289. value = value.__func__
  290. is_classmethod = True
  291. @_pytest_fn_decorator
  292. def make_async(fn, *args, **kwargs):
  293. return asyncio._maybe_async(fn, *args, **kwargs)
  294. do_async = make_async(value)
  295. if is_classmethod:
  296. do_async = classmethod(do_async)
  297. do_async._maybe_async_applied = True
  298. setattr(obj, name, do_async)
  299. if recurse:
  300. for cls in obj.mro()[1:]:
  301. if cls != object:
  302. _apply_maybe_async(cls, False)
  303. return obj
  304. def _parametrize_cls(module, cls):
  305. """implement a class-based version of pytest parametrize."""
  306. if "_sa_parametrize" not in cls.__dict__:
  307. return [cls]
  308. _sa_parametrize = cls._sa_parametrize
  309. classes = []
  310. for full_param_set in itertools.product(
  311. *[params for argname, params in _sa_parametrize]
  312. ):
  313. cls_variables = {}
  314. for argname, param in zip(
  315. [_sa_param[0] for _sa_param in _sa_parametrize], full_param_set
  316. ):
  317. if not argname:
  318. raise TypeError("need argnames for class-based combinations")
  319. argname_split = re.split(r",\s*", argname)
  320. for arg, val in zip(argname_split, param.values):
  321. cls_variables[arg] = val
  322. parametrized_name = "_".join(
  323. re.sub(r"\W", "", token)
  324. for param in full_param_set
  325. for token in param.id.split("-")
  326. )
  327. name = "%s_%s" % (cls.__name__, parametrized_name)
  328. newcls = type.__new__(type, name, (cls,), cls_variables)
  329. setattr(module, name, newcls)
  330. classes.append(newcls)
  331. return classes
  332. _current_class = None
  333. def pytest_runtest_setup(item):
  334. from sqlalchemy.testing import asyncio
  335. # pytest_runtest_setup runs *before* pytest fixtures with scope="class".
  336. # plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
  337. # for the whole class and has to run things that are across all current
  338. # databases, so we run this outside of the pytest fixture system altogether
  339. # and ensure asyncio greenlet if any engines are async
  340. global _current_class
  341. if isinstance(item, pytest.Function) and _current_class is None:
  342. asyncio._maybe_async_provisioning(
  343. plugin_base.start_test_class_outside_fixtures,
  344. item.cls,
  345. )
  346. _current_class = item.getparent(pytest.Class)
  347. @pytest.hookimpl(hookwrapper=True)
  348. def pytest_runtest_teardown(item, nextitem):
  349. # runs inside of pytest function fixture scope
  350. # after test function runs
  351. from sqlalchemy.testing import asyncio
  352. asyncio._maybe_async(plugin_base.after_test, item)
  353. yield
  354. # this is now after all the fixture teardown have run, the class can be
  355. # finalized. Since pytest v7 this finalizer can no longer be added in
  356. # pytest_runtest_setup since the class has not yet been setup at that
  357. # time.
  358. # See https://github.com/pytest-dev/pytest/issues/9343
  359. global _current_class, _current_report
  360. if _current_class is not None and (
  361. # last test or a new class
  362. nextitem is None
  363. or nextitem.getparent(pytest.Class) is not _current_class
  364. ):
  365. _current_class = None
  366. try:
  367. asyncio._maybe_async_provisioning(
  368. plugin_base.stop_test_class_outside_fixtures, item.cls
  369. )
  370. except Exception as e:
  371. # in case of an exception during teardown attach the original
  372. # error to the exception message, otherwise it will get lost
  373. if _current_report.failed:
  374. if not e.args:
  375. e.args = (
  376. "__Original test failure__:\n"
  377. + _current_report.longreprtext,
  378. )
  379. elif e.args[-1] and isinstance(e.args[-1], str):
  380. args = list(e.args)
  381. args[-1] += (
  382. "\n__Original test failure__:\n"
  383. + _current_report.longreprtext
  384. )
  385. e.args = tuple(args)
  386. else:
  387. e.args += (
  388. "__Original test failure__",
  389. _current_report.longreprtext,
  390. )
  391. raise
  392. finally:
  393. _current_report = None
  394. def pytest_runtest_call(item):
  395. # runs inside of pytest function fixture scope
  396. # before test function runs
  397. from sqlalchemy.testing import asyncio
  398. asyncio._maybe_async(
  399. plugin_base.before_test,
  400. item,
  401. item.module.__name__,
  402. item.cls,
  403. item.name,
  404. )
  405. _current_report = None
  406. def pytest_runtest_logreport(report):
  407. global _current_report
  408. if report.when == "call":
  409. _current_report = report
  410. @pytest.fixture(scope="class")
  411. def setup_class_methods(request):
  412. from sqlalchemy.testing import asyncio
  413. cls = request.cls
  414. if hasattr(cls, "setup_test_class"):
  415. asyncio._maybe_async(cls.setup_test_class)
  416. yield
  417. if hasattr(cls, "teardown_test_class"):
  418. asyncio._maybe_async(cls.teardown_test_class)
  419. asyncio._maybe_async(plugin_base.stop_test_class, cls)
  420. @pytest.fixture(scope="function")
  421. def setup_test_methods(request):
  422. from sqlalchemy.testing import asyncio
  423. # called for each test
  424. self = request.instance
  425. # before this fixture runs:
  426. # 1. function level "autouse" fixtures under py3k (examples: TablesTest
  427. # define tables / data, MappedTest define tables / mappers / data)
  428. # 2. was for p2k. no longer applies
  429. # 3. run outer xdist-style setup
  430. if hasattr(self, "setup_test"):
  431. asyncio._maybe_async(self.setup_test)
  432. # alembic test suite is using setUp and tearDown
  433. # xdist methods; support these in the test suite
  434. # for the near term
  435. if hasattr(self, "setUp"):
  436. asyncio._maybe_async(self.setUp)
  437. # inside the yield:
  438. # 4. function level fixtures defined on test functions themselves,
  439. # e.g. "connection", "metadata" run next
  440. # 5. pytest hook pytest_runtest_call then runs
  441. # 6. test itself runs
  442. yield
  443. # yield finishes:
  444. # 7. function level fixtures defined on test functions
  445. # themselves, e.g. "connection" rolls back the transaction, "metadata"
  446. # emits drop all
  447. # 8. pytest hook pytest_runtest_teardown hook runs, this is associated
  448. # with fixtures close all sessions, provisioning.stop_test_class(),
  449. # engines.testing_reaper -> ensure all connection pool connections
  450. # are returned, engines created by testing_engine that aren't the
  451. # config engine are disposed
  452. asyncio._maybe_async(plugin_base.after_test_fixtures, self)
  453. # 10. run xdist-style teardown
  454. if hasattr(self, "tearDown"):
  455. asyncio._maybe_async(self.tearDown)
  456. if hasattr(self, "teardown_test"):
  457. asyncio._maybe_async(self.teardown_test)
  458. # 11. was for p2k. no longer applies
  459. # 12. function level "autouse" fixtures under py3k (examples: TablesTest /
  460. # MappedTest delete table data, possibly drop tables and clear mappers
  461. # depending on the flags defined by the test class)
  462. def _pytest_fn_decorator(target):
  463. """Port of langhelpers.decorator with pytest-specific tricks."""
  464. from sqlalchemy.util.langhelpers import format_argspec_plus
  465. from sqlalchemy.util.compat import inspect_getfullargspec
  466. def _exec_code_in_env(code, env, fn_name):
  467. # note this is affected by "from __future__ import annotations" at
  468. # the top; exec'ed code will use non-evaluated annotations
  469. # which allows us to be more flexible with code rendering
  470. # in format_argpsec_plus()
  471. exec(code, env)
  472. return env[fn_name]
  473. def decorate(fn, add_positional_parameters=()):
  474. spec = inspect_getfullargspec(fn)
  475. if add_positional_parameters:
  476. spec.args.extend(add_positional_parameters)
  477. metadata = dict(
  478. __target_fn="__target_fn", __orig_fn="__orig_fn", name=fn.__name__
  479. )
  480. metadata.update(format_argspec_plus(spec, grouped=False))
  481. code = (
  482. """\
  483. def %(name)s%(grouped_args)s:
  484. return %(__target_fn)s(%(__orig_fn)s, %(apply_kw)s)
  485. """
  486. % metadata
  487. )
  488. decorated = _exec_code_in_env(
  489. code, {"__target_fn": target, "__orig_fn": fn}, fn.__name__
  490. )
  491. if not add_positional_parameters:
  492. decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__
  493. decorated.__wrapped__ = fn
  494. return update_wrapper(decorated, fn)
  495. else:
  496. # this is the pytest hacky part. don't do a full update wrapper
  497. # because pytest is really being sneaky about finding the args
  498. # for the wrapped function
  499. decorated.__module__ = fn.__module__
  500. decorated.__name__ = fn.__name__
  501. if hasattr(fn, "pytestmark"):
  502. decorated.pytestmark = fn.pytestmark
  503. return decorated
  504. return decorate
  505. class PytestFixtureFunctions(plugin_base.FixtureFunctions):
  506. def skip_test_exception(self, *arg, **kw):
  507. return pytest.skip.Exception(*arg, **kw)
  508. @property
  509. def add_to_marker(self):
  510. return pytest.mark
  511. def mark_base_test_class(self):
  512. return pytest.mark.usefixtures(
  513. "setup_class_methods", "setup_test_methods"
  514. )
  515. _combination_id_fns = {
  516. "i": lambda obj: obj,
  517. "r": repr,
  518. "s": str,
  519. "n": lambda obj: (
  520. obj.__name__ if hasattr(obj, "__name__") else type(obj).__name__
  521. ),
  522. }
  523. def combinations(self, *arg_sets, **kw):
  524. """Facade for pytest.mark.parametrize.
  525. Automatically derives argument names from the callable which in our
  526. case is always a method on a class with positional arguments.
  527. ids for parameter sets are derived using an optional template.
  528. """
  529. from sqlalchemy.testing import exclusions
  530. if len(arg_sets) == 1 and hasattr(arg_sets[0], "__next__"):
  531. arg_sets = list(arg_sets[0])
  532. argnames = kw.pop("argnames", None)
  533. def _filter_exclusions(args):
  534. result = []
  535. gathered_exclusions = []
  536. for a in args:
  537. if isinstance(a, exclusions.compound):
  538. gathered_exclusions.append(a)
  539. else:
  540. result.append(a)
  541. return result, gathered_exclusions
  542. id_ = kw.pop("id_", None)
  543. tobuild_pytest_params = []
  544. has_exclusions = False
  545. if id_:
  546. _combination_id_fns = self._combination_id_fns
  547. # because itemgetter is not consistent for one argument vs.
  548. # multiple, make it multiple in all cases and use a slice
  549. # to omit the first argument
  550. _arg_getter = operator.itemgetter(
  551. 0,
  552. *[
  553. idx
  554. for idx, char in enumerate(id_)
  555. if char in ("n", "r", "s", "a")
  556. ],
  557. )
  558. fns = [
  559. (operator.itemgetter(idx), _combination_id_fns[char])
  560. for idx, char in enumerate(id_)
  561. if char in _combination_id_fns
  562. ]
  563. for arg in arg_sets:
  564. if not isinstance(arg, tuple):
  565. arg = (arg,)
  566. fn_params, param_exclusions = _filter_exclusions(arg)
  567. parameters = _arg_getter(fn_params)[1:]
  568. if param_exclusions:
  569. has_exclusions = True
  570. tobuild_pytest_params.append(
  571. (
  572. parameters,
  573. param_exclusions,
  574. "-".join(
  575. comb_fn(getter(arg)) for getter, comb_fn in fns
  576. ),
  577. )
  578. )
  579. else:
  580. for arg in arg_sets:
  581. if not isinstance(arg, tuple):
  582. arg = (arg,)
  583. fn_params, param_exclusions = _filter_exclusions(arg)
  584. if param_exclusions:
  585. has_exclusions = True
  586. tobuild_pytest_params.append(
  587. (fn_params, param_exclusions, None)
  588. )
  589. pytest_params = []
  590. for parameters, param_exclusions, id_ in tobuild_pytest_params:
  591. if has_exclusions:
  592. parameters += (param_exclusions,)
  593. param = pytest.param(*parameters, id=id_)
  594. pytest_params.append(param)
  595. def decorate(fn):
  596. if inspect.isclass(fn):
  597. if has_exclusions:
  598. raise NotImplementedError(
  599. "exclusions not supported for class level combinations"
  600. )
  601. if "_sa_parametrize" not in fn.__dict__:
  602. fn._sa_parametrize = []
  603. fn._sa_parametrize.append((argnames, pytest_params))
  604. return fn
  605. else:
  606. _fn_argnames = inspect.getfullargspec(fn).args[1:]
  607. if argnames is None:
  608. _argnames = _fn_argnames
  609. else:
  610. _argnames = re.split(r", *", argnames)
  611. if has_exclusions:
  612. existing_exl = sum(
  613. 1 for n in _fn_argnames if n.startswith("_exclusions")
  614. )
  615. current_exclusion_name = f"_exclusions_{existing_exl}"
  616. _argnames += [current_exclusion_name]
  617. @_pytest_fn_decorator
  618. def check_exclusions(fn, *args, **kw):
  619. _exclusions = args[-1]
  620. if _exclusions:
  621. exlu = exclusions.compound().add(*_exclusions)
  622. fn = exlu(fn)
  623. return fn(*args[:-1], **kw)
  624. fn = check_exclusions(
  625. fn, add_positional_parameters=(current_exclusion_name,)
  626. )
  627. return pytest.mark.parametrize(_argnames, pytest_params)(fn)
  628. return decorate
  629. def param_ident(self, *parameters):
  630. ident = parameters[0]
  631. return pytest.param(*parameters[1:], id=ident)
  632. def fixture(self, *arg, **kw):
  633. from sqlalchemy.testing import config
  634. from sqlalchemy.testing import asyncio
  635. # wrapping pytest.fixture function. determine if
  636. # decorator was called as @fixture or @fixture().
  637. if len(arg) > 0 and callable(arg[0]):
  638. # was called as @fixture(), we have the function to wrap.
  639. fn = arg[0]
  640. arg = arg[1:]
  641. else:
  642. # was called as @fixture, don't have the function yet.
  643. fn = None
  644. # create a pytest.fixture marker. because the fn is not being
  645. # passed, this is always a pytest.FixtureFunctionMarker()
  646. # object (or whatever pytest is calling it when you read this)
  647. # that is waiting for a function.
  648. fixture = pytest.fixture(*arg, **kw)
  649. # now apply wrappers to the function, including fixture itself
  650. def wrap(fn):
  651. if config.any_async:
  652. fn = asyncio._maybe_async_wrapper(fn)
  653. # other wrappers may be added here
  654. # now apply FixtureFunctionMarker
  655. fn = fixture(fn)
  656. return fn
  657. if fn:
  658. return wrap(fn)
  659. else:
  660. return wrap
  661. def get_current_test_name(self):
  662. return os.environ.get("PYTEST_CURRENT_TEST")
  663. def async_test(self, fn):
  664. from sqlalchemy.testing import asyncio
  665. @_pytest_fn_decorator
  666. def decorate(fn, *args, **kwargs):
  667. asyncio._run_coroutine_function(fn, *args, **kwargs)
  668. return decorate(fn)