Skip to content

Commit b3fc5e8

Browse files
authored
Drop marshmallow 3 (#461)
* Drop marshmallow 3 and deserialize default values * Revert deserialization change
1 parent b7bdc47 commit b3fc5e8

9 files changed

Lines changed: 39 additions & 51 deletions

File tree

‎.github/workflows/build-release.yml‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
include:
16-
- { name: "3.10", tox: py310-marshmallow3 }
17-
- { name: "3.14", tox: py314-marshmallow3 }
16+
- { name: "3.10", tox: py310-marshmallowdev }
17+
- { name: "3.14", tox: py314-marshmallowdev }
1818
- { name: "lowest", tox: py310-marshmallowlowest }
19-
- { name: "highest", tox: py314-marshmallowdev }
20-
- { name: "mypy-ma3", tox: mypy-marshmallow3 }
2119
- { name: "mypy-madev", tox: mypy-marshmallowdev }
2220
steps:
2321
- uses: actions/checkout@v6

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## unreleased
4+
5+
Other changes:
6+
7+
- Drop support for marshmallow 3. marshmallow>=4.0.0 is now required.
8+
39
## 14.6.0 (2026-02-19)
410

511
Bug fixes:

‎pyproject.toml‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ name = "environs"
33
version = "14.6.0"
44
description = "simplified environment variable parsing"
55
readme = "README.md"
6-
license = { file = "LICENSE" }
6+
license = "MIT"
77
authors = [{ name = "Steven Loria", email = "oss@stevenloria.com" }]
88
classifiers = [
99
"Intended Audience :: Developers",
10-
"License :: OSI Approved :: MIT License",
1110
"Natural Language :: English",
1211
"Programming Language :: Python :: 3",
1312
"Programming Language :: Python :: 3.10",
@@ -19,7 +18,7 @@ classifiers = [
1918
requires-python = ">=3.10"
2019
dependencies = [
2120
"python-dotenv",
22-
"marshmallow>=3.26.2",
21+
"marshmallow>=4.0.0",
2322
"typing-extensions; python_version < '3.11'",
2423
]
2524

@@ -34,17 +33,11 @@ django = ["dj-database-url", "dj-email-url", "django-cache-url"]
3433
[dependency-groups]
3534
django = ["dj-database-url", "dj-email-url", "django-cache-url"]
3635
tests = [
37-
{include-group = "django"},
36+
{ include-group = "django" },
3837
"pytest",
39-
"packaging",
4038
"backports.strenum; python_version < '3.11'",
4139
]
42-
dev = [
43-
{include-group = "tests"},
44-
"tox",
45-
"tox-uv",
46-
"pre-commit>=4.0,<5.0",
47-
]
40+
dev = [{ include-group = "tests" }, "tox", "tox-uv", "pre-commit>=4.0,<5.0"]
4841

4942
[build-system]
5043
requires = ["flit_core<4"]
@@ -74,6 +67,7 @@ ignore = [
7467
"ARG", # unused arguments are common w/ interfaces
7568
"COM", # let formatter take care commas
7669
"C901", # don't enforce complexity level
70+
"PLR0912", # don't enforce max branches
7771
"D", # don't require docstrings
7872
"E501", # leave line-length enforcement to formatter
7973
"EM", # allow string messages in exceptions
@@ -99,10 +93,6 @@ ignore = [
9993
"T", # allow prints
10094
]
10195

102-
103-
[tool.ruff.lint.pycodestyle]
104-
ignore-overlong-task-comments = true
105-
10696
[tool.mypy]
10797
files = ["src", "tests"]
10898
ignore_missing_imports = true

‎src/environs/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def _make_subcast_field(
199199

200200
class SubcastField(ma.fields.Field):
201201
def _deserialize(self, value, *args, **kwargs):
202+
if not isinstance(value, str):
203+
return value
202204
return subcast(value)
203205

204206
inner_field = SubcastField

‎src/environs/fields.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
from marshmallow import ValidationError, fields
1717

1818

19-
# TODO: Change to ma.fields.Field[Path] after dropping marshmallow 3 support
20-
class Path(fields.Field):
19+
class Path(fields.Field[pathlib.Path]):
2120
def _serialize(self, value: pathlib.Path | None, *args, **kwargs) -> str | None:
2221
if value is None:
2322
return None
@@ -151,5 +150,7 @@ def deserialize( # type: ignore[override]
151150
data: typing.Mapping[str, typing.Any] | None = None,
152151
**kwargs,
153152
) -> ParseResult:
153+
if isinstance(value, ParseResult):
154+
return value
154155
ret = typing.cast("str", super().deserialize(value, attr, data, **kwargs))
155156
return urlparse(ret)

‎tests/mypy_test_cases/env.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
33
To run these, use: ::
44
5-
tox -e mypy-marshmallow3
6-
7-
Or ::
8-
95
tox -e mypy-marshmallowdev
106
"""
117

‎tests/test_environs.py‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import datetime as dt
2-
import importlib.metadata
32
import logging
43
import os
54
import pathlib
@@ -20,13 +19,11 @@
2019
import marshmallow as ma
2120
import pytest
2221
from marshmallow import fields
23-
from packaging.version import Version
2422

2523
import environs
2624
from environs import validate
2725

2826
HERE = pathlib.Path(__file__).parent
29-
MARSHMALLOW_VERSION = Version(importlib.metadata.version("marshmallow"))
3027

3128

3229
@pytest.fixture
@@ -265,14 +262,24 @@ def test_default_set_to_internal_type(
265262
method = getattr(env, method_name)
266263
assert method("NOTFOUND", value) == value
267264

265+
def test_url_with_parseresult_default(self, env: environs.Env):
266+
default = urllib.parse.urlparse("https://example.com")
267+
result = env.url("UNSET_URL", default=default)
268+
assert result == default
269+
assert isinstance(result, urllib.parse.ParseResult)
270+
271+
def test_none_default_unchanged(self, env: environs.Env):
272+
assert env.int("UNSET_NONE1", default=None) is None
273+
assert env.list("UNSET_NONE2", default=None) is None
274+
assert env.timedelta("UNSET_NONE3", default=None) is None
275+
268276
def test_timedelta_cast(self, set_env, env: environs.Env):
269-
# marshmallow 4 preserves float values as microseconds
270-
if MARSHMALLOW_VERSION.major >= 4:
271-
set_env({"TIMEDELTA": "42.9"})
272-
assert env.timedelta("TIMEDELTA") == dt.timedelta(
273-
seconds=42,
274-
microseconds=900000,
275-
)
277+
# marshmallow preserves float values as microseconds
278+
set_env({"TIMEDELTA": "42.9"})
279+
assert env.timedelta("TIMEDELTA") == dt.timedelta(
280+
seconds=42,
281+
microseconds=900000,
282+
)
276283
# seconds as integer
277284
set_env({"TIMEDELTA": "0"})
278285
assert env.timedelta("TIMEDELTA") == dt.timedelta()

‎tox.ini‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
[tox]
22
envlist=
33
lint
4-
py{310,311,312,313,314}-marshmallow{3,lowest,dev}
5-
mypy-marshmallow{3,dev}
4+
py{310,311,312,313,314}-marshmallow{lowest,dev}
5+
mypy-marshmallowdev
66

77
[testenv]
88
dependency_groups = tests
99
deps =
10-
marshmallowlowest: marshmallow==3.26.2;python_version<"3.12"
11-
marshmallow3: marshmallow>=3.26.2,<4.0.0
10+
marshmallowlowest: marshmallow==4.0.0
1211
marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
1312
commands = pytest {posargs}
1413

15-
[testenv:mypy-marshmallow3]
16-
dependency_groups = django
17-
deps =
18-
mypy
19-
marshmallow>=3.26.2,<4.0.0
20-
commands = mypy
21-
2214
[testenv:mypy-marshmallowdev]
2315
dependency_groups = django
2416
deps =

‎uv.lock‎

Lines changed: 1 addition & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)