pytestplugin.py 27 KB

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