Skip to content

Commit 4774880

Browse files
authored
Merge pull request #132 from schireson/dc/pyproject_config
fix: Handle pyproject.toml based alembic config.
2 parents 75f5b93 + cfed94d commit 4774880

File tree

4 files changed

+122
-48
lines changed

4 files changed

+122
-48
lines changed

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
# Changelog
22

3-
## [Unreleased](https://github.com/schireson/pytest-alembic/compare/v0.11.0...HEAD) (2025-05-16)
3+
### [v0.12.1](https://github.com/schireson/pytest-alembic/compare/v0.12.0...v0.12.1) (2025-05-22)
4+
5+
#### Fixes
6+
7+
* Handle pyproject.toml based alembic config.
8+
([82d9d62](https://github.com/schireson/pytest-alembic/commit/82d9d62024ccb86b655272a70e0d94374b5675a5))
9+
10+
## [v0.12.0](https://github.com/schireson/pytest-alembic/compare/v0.11.1...v0.12.0) (2025-05-16)
411

512
### Fixes
613

714
* Release 0.12.0
8-
([87dbf29](https://github.com/schireson/pytest-alembic/commit/87dbf29e24f47417a5821fb6908673058ead7d89))
15+
([4baee1e](https://github.com/schireson/pytest-alembic/commit/4baee1e85abe3733e6aa785c7704248f4fbb59b0))
916
* Failing tests.
1017
([1dc5b43](https://github.com/schireson/pytest-alembic/commit/1dc5b43d7e5b7530181808490cb31386631990a9))
1118
* Linting.
@@ -17,6 +24,13 @@
1724
* Updated pyproject.toml to be more flexible with poetry_core versioning.
1825
([c7f25c3](https://github.com/schireson/pytest-alembic/commit/c7f25c39a795591a794f1ab89203f4a992012319))
1926

27+
### [v0.11.1](https://github.com/schireson/pytest-alembic/compare/v0.11.0...v0.11.1) (2024-03-27)
28+
29+
#### Fixes
30+
31+
* Ensure branched revisions are upgraded individually once.
32+
([25b03a3](https://github.com/schireson/pytest-alembic/commit/25b03a3cac04258bbf3725f00e4794663808581a))
33+
2034
## [v0.11.0](https://github.com/schireson/pytest-alembic/compare/v0.10.7...v0.11.0) (2024-03-04)
2135

2236
### Fixes

pyproject.toml

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
11
[tool.poetry]
22
name = "pytest-alembic"
3-
version = "0.12.0"
3+
version = "0.12.1"
44
description = "A pytest plugin for verifying alembic migrations."
5-
authors = [
6-
"Dan Cardin <[email protected]>",
7-
]
5+
authors = ["Dan Cardin <[email protected]>"]
86
license = "MIT"
9-
keywords = [ "pytest", "sqlalchemy", "alembic", "migration", "revision" ]
10-
classifiers = [ "Framework :: Pytest" ]
7+
keywords = ["pytest", "sqlalchemy", "alembic", "migration", "revision"]
8+
classifiers = ["Framework :: Pytest"]
119
repository = "https://github.com/schireson/pytest-alembic"
12-
packages = [
13-
{ include = "pytest_alembic", from = "src" },
14-
]
10+
packages = [{ include = "pytest_alembic", from = "src" }]
1511
readme = 'README.md'
16-
include = [
17-
"*.md",
18-
]
12+
include = ["*.md"]
1913

2014
[tool.poetry.dependencies]
2115
python = ">=3.9, <4"
2216

23-
pytest = {version = ">=7.0"}
17+
pytest = { version = ">=7.0" }
2418
alembic = "*"
2519
sqlalchemy = "*"
2620

2721
[tool.poetry.group.dev.dependencies]
2822
asyncpg = "*"
29-
black = {version = "22.3.0", python = ">=3.6.2"}
30-
coverage = {version = ">=6.4.4", extras = ["toml"]}
23+
black = { version = "22.3.0", python = ">=3.6.2" }
24+
coverage = { version = ">=6.4.4", extras = ["toml"] }
3125
greenlet = ">=3.0"
32-
mypy = {version = ">=1.11", python = ">=3.8"}
26+
mypy = { version = ">=1.11", python = ">=3.8" }
3327
psycopg2-binary = "*"
34-
pytest = {version = ">=8.3.2", python = ">=3.8"}
28+
pytest = { version = ">=8.3.2", python = ">=3.8" }
3529
pytest-asyncio = "*"
36-
pytest-mock-resources = {version = ">=2.6.3", extras = ["docker"]}
37-
ruff = {version = '0.0.269'}
38-
sqlalchemy = {version = ">=1.4", extras = ["asyncio"]}
30+
pytest-mock-resources = { version = ">=2.6.3", extras = ["docker"] }
31+
ruff = { version = '0.0.269' }
32+
sqlalchemy = { version = ">=1.4", extras = ["asyncio"] }
3933
types-dataclasses = "^0.1.7"
4034

4135
[tool.poetry.plugins.pytest11]
@@ -47,21 +41,54 @@ line_length = 100
4741
[tool.coverage.report]
4842
show_missing = true
4943
skip_covered = true
50-
exclude_lines = [
51-
"pragma: no cover",
52-
"if TYPE_CHECKING:",
53-
"if __name__ == .__main__.:",
54-
]
44+
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.:"]
5545

5646
[tool.ruff]
5747
src = ["src", "tests"]
5848
target-version = "py37"
5949
line-length = 100
6050
select = [
61-
"A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ARG", "BLE",
62-
"DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH",
63-
"PIE", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID",
64-
"TRY", "UP", "YTT"
51+
"A",
52+
"B",
53+
"C",
54+
"D",
55+
"E",
56+
"F",
57+
"G",
58+
"I",
59+
"N",
60+
"Q",
61+
"S",
62+
"T",
63+
"W",
64+
"ARG",
65+
"BLE",
66+
"DJ",
67+
"DTZ",
68+
"EM",
69+
"ERA",
70+
"EXE",
71+
"FBT",
72+
"ICN",
73+
"INP",
74+
"ISC",
75+
"NPY",
76+
"PD",
77+
"PGH",
78+
"PIE",
79+
"PT",
80+
"PTH",
81+
"PYI",
82+
"RET",
83+
"RSE",
84+
"RUF",
85+
"SIM",
86+
"SLF",
87+
"TCH",
88+
"TID",
89+
"TRY",
90+
"UP",
91+
"YTT",
6592
]
6693
ignore = ["E501", "S101", "D1", "TRY200", "B007", "B904"]
6794

@@ -95,7 +122,7 @@ filterwarnings = [
95122
"ignore:unclosed connection.*:ResourceWarning",
96123
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",
97124
"ignore:.*is deprecated and will be removed in Python 3.14.*:DeprecationWarning",
98-
]
125+
]
99126

100127
[tool.mypy]
101128
strict_optional = true

src/pytest_alembic/config.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
33

44
import alembic.config
5-
from alembic.util import immutabledict
65

76
if TYPE_CHECKING:
7+
from alembic.util import immutabledict
8+
89
from pytest_alembic.revision_data import RevisionSpec
910

1011

@@ -104,7 +105,7 @@ def from_raw_config(
104105
)
105106

106107
def make_alembic_config(self, stdout):
107-
file = (
108+
ini_file = (
108109
self.config_options.get("file")
109110
or self.config_options.get("config_file_name")
110111
or "alembic.ini"
@@ -116,19 +117,24 @@ def make_alembic_config(self, stdout):
116117
alembic_config = self.alembic_config
117118
alembic_config.stdout = stdout
118119
else:
119-
alembic_config = alembic.config.Config(file, stdout=stdout)
120+
if _supports_toml():
121+
alembic_config = alembic.config.Config(
122+
file_=ini_file, toml_file="pyproject.toml", stdout=stdout
123+
)
124+
else:
125+
alembic_config = alembic.config.Config(ini_file, stdout=stdout)
120126

121127
sqlalchemy_url = self.config_options.get("sqlalchemy.url")
122128
if sqlalchemy_url:
123129
alembic_config.set_main_option("sqlalchemy.url", sqlalchemy_url)
124130

125-
# Only set script_location if set.
126131
script_location = self.config_options.get("script_location")
127-
if script_location:
128-
alembic_config.set_section_option("alembic", "script_location", script_location)
129-
elif not alembic_config.get_section_option("alembic", "script_location"):
132+
if not script_location:
130133
# Or in the event that it's not set after already having loaded the config.
131-
alembic_config.set_main_option("script_location", "migrations")
134+
script_location = self._get_option(
135+
alembic_config, "script_location", default="migrations"
136+
)
137+
alembic_config.set_main_option("script_location", script_location)
132138

133139
target_metadata = self.config_options.get("target_metadata")
134140
alembic_config.attributes["target_metadata"] = target_metadata
@@ -141,6 +147,13 @@ def make_alembic_config(self, stdout):
141147

142148
return alembic_config
143149

150+
@staticmethod
151+
def _get_option(alembic_config: alembic.config.Config, key: str, *, default: str) -> str:
152+
if _supports_toml():
153+
get_alembic_option = getattr(alembic_config, "get_alembic_option") # noqa: B009
154+
return get_alembic_option(key, default)
155+
return alembic_config.get_main_option(key, default)
156+
144157

145158
def duplicate_alembic_config(config: alembic.config.Config):
146159
return alembic.config.Config(
@@ -149,6 +162,10 @@ def duplicate_alembic_config(config: alembic.config.Config):
149162
output_buffer=config.output_buffer,
150163
stdout=config.stdout,
151164
cmd_opts=config.cmd_opts,
152-
config_args=cast(immutabledict, config.config_args),
165+
config_args=cast("immutabledict", config.config_args),
153166
attributes=config.attributes,
154167
)
168+
169+
170+
def _supports_toml() -> bool:
171+
return hasattr(alembic.config.Config, "get_alembic_option")

tests/test_config.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,37 @@
11
from unittest import mock
22

3+
import alembic.config
4+
import pytest
5+
36
from pytest_alembic.config import Config
47

58

6-
def mock_open(*args, **kargs):
7-
f_open = mock.mock_open(*args, **kargs)
8-
f_open.return_value.__iter__ = lambda self: iter(self.readline, "")
9-
return f_open
9+
def mock_patch(*contents):
10+
m_open = mock.mock_open()
11+
m_open.side_effect = [(mock.mock_open(read_data=string).return_value) for string in contents]
12+
return mock.patch("builtins.open", m_open)
1013

1114

1215
def test_default_file():
1316
config = Config()
1417

15-
with mock.patch("builtins.open", mock_open(read_data="[alembic]")):
18+
with mock_patch("[alembic]", b""):
1619
alembic_config = config.make_alembic_config(None)
1720
assert alembic_config.config_file_name == "alembic.ini"
1821

1922

2023
def test_set_file():
2124
config = Config(config_options={"file": "foo.ini"})
2225

23-
with mock.patch("builtins.open", mock_open(read_data="[alembic]")):
26+
with mock_patch("[alembic]", b""):
2427
alembic_config = config.make_alembic_config(None)
2528
assert alembic_config.config_file_name == "foo.ini"
2629

2730

2831
def test_set_config_file_name():
2932
config = Config(config_options={"config_file_name": "foo.ini"})
3033

31-
with mock.patch("builtins.open", mock_open(read_data="[alembic]")):
34+
with mock_patch("[alembic]", b""):
3235
alembic_config = config.make_alembic_config(None)
3336
assert alembic_config.config_file_name == "foo.ini"
3437

@@ -43,3 +46,16 @@ def test_set_script_location():
4346
config = Config(config_options={"script_location": "alembic"})
4447
alembic_config = config.make_alembic_config(None)
4548
assert alembic_config.get_main_option("script_location") == "alembic"
49+
50+
51+
@pytest.mark.skipif(
52+
not hasattr(alembic.config.Config, "get_alembic_option"),
53+
reason="pyproject.toml is not supported in this version of alembic",
54+
)
55+
def test_pyproject_toml():
56+
config = Config()
57+
58+
exists = mock.patch("alembic.config.Config._toml_file_path", return_value=True)
59+
with exists, mock_patch("[alembic]", b"[tool.alembic]\nscript_location = 'asdf'"):
60+
alembic_config = config.make_alembic_config(None)
61+
assert alembic_config.get_main_option("script_location") == "asdf"

0 commit comments

Comments
 (0)