Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,46 @@ on: [push, pull_request]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python -
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python -

- name: Check dependencies
run: make doctor
- name: Check dependencies
run: make doctor

- uses: actions/cache@v4
with:
path: .venv
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
- uses: actions/cache@v4
with:
path: .venv
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}

- name: Install dependencies
run: make install
- name: Install dependencies
run: make install

- name: Test code
run: make test-repeat
- name: Test code
run: make test-repeat

- name: Upload coverage
uses: codecov/codecov-action@v4
if: steps.fork-check.outputs.is-fork == 'false'
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage
uses: codecov/codecov-action@v4
if: steps.fork-check.outputs.is-fork == 'false'
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

- name: Check code
run: make check
- name: Check code
run: make check

- name: Check documentation
run: make mkdocs notebooks
- name: Check documentation
run: make mkdocs notebooks
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
python 3.13.11
python 3.14.2
poetry 2.3.0
141 changes: 141 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{
"files.exclude": {
".cache/": true,
".venv/": true,
"*.egg-info": true,
"pip-wheel-metadata/": true,
"**/__pycache__": true,
"**/*.pyc": true,
"**/.ipynb_checkpoints": true,
"**/tmp/": true,
"dist/": true,
"htmlcov/": true,
"prof/": true,
"site/": true,
"geckodriver.log": true
},
"python.defaultInterpreterPath": ".venv/bin/python",
"editor.formatOnSave": true,
"pylint.path": [
".venv/bin/pylint"
],
"pylint.args": [
"--rcfile=.pylint.ini"
],
"cSpell.words": [
"abatilo",
"addopts",
"appex",
"autorefs",
"brandonaut",
"brosche",
"builtins",
"choco",
"classproperties",
"codehilite",
"completly",
"cookiecutter",
"coveragerc",
"coveragespace",
"currentdir",
"cygstart",
"cygwin",
"dataclass",
"dataclasses",
"datafile",
"ensurepip",
"Etrange",
"exitfirst",
"findstr",
"fontawesome",
"freezegun",
"gethostname",
"getpid",
"getplugin",
"gitman",
"gitsvn",
"gittoplevel",
"Graphviz",
"gunechristensen",
"hdnivara",
"htmlcov",
"iglob",
"imac",
"importlib",
"ioreg",
"iphoto",
"ipynb",
"ipython",
"isort",
"Jace",
"lastfailed",
"levelname",
"logbreak",
"macbook",
"macfsevents",
"mastupristi",
"maxfail",
"maxsplit",
"MDEF",
"minilog",
"mkdocs",
"mkdocstrings",
"mrpossoms",
"mttjohnson",
"mylink",
"mypy",
"nbstripout",
"netrc",
"nobeep",
"noclasses",
"nohup",
"onpass",
"papermill",
"pdbcls",
"pipx",
"pluginmanager",
"preserialization",
"Preserialized",
"Preserializing",
"psutil",
"pydocstyle",
"pygments",
"pyinstaller",
"pylint",
"pync",
"pyproject",
"pyreverse",
"pytest",
"rcfile",
"relpath",
"repr",
"ruamel",
"rustup",
"scalarstring",
"sergey",
"setuptools",
"showfspath",
"shuyskiy",
"spurnvoj",
"startfile",
"tomlkit",
"tput",
"tracebackhide",
"Trilean",
"trufflehog",
"udevadm",
"unparseable",
"unstaged",
"USERPROFILE",
"venv",
"verchew",
"verchewrc",
"virtualenvs",
"webfonts",
"xenji",
"xfail",
"YORM"
],
"makefile.configureOnOpen": false
}

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes

## 2.5 (2026-01-29)

- Added support for Python 3.14.

## 2.4 (2026-01-19)

- Dropped support for Python 3.9.
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Datafiles is a bidirectional serialization library for Python [dataclasses](http
[![PyPI License](https://img.shields.io/pypi/l/datafiles.svg)](https://pypi.org/project/datafiles)
[![PyPI Version](https://img.shields.io/pypi/v/datafiles.svg?label=version)](https://pypi.org/project/datafiles)
[![PyPI Downloads](https://img.shields.io/pypi/dm/datafiles.svg?color=orange)](https://pypistats.org/packages/datafiles)
[![Gitter](https://img.shields.io/gitter/room/jacebrowning/datafiles?color=D0164E)](https://gitter.im/jacebrowning/datafiles)

Some common use cases include:

Expand Down
42 changes: 28 additions & 14 deletions datafiles/converters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ def register(cls: Union[type, str], converter: type):
register(dict, Dictionary)


def _convert_union(args: tuple[type, ...]) -> type | None:
"""Return converter for currently supported unions."""
if len(args) != 2:
return None
none_type = type(None)
if args[0] is none_type or args[1] is none_type:
value_type = args[1] if args[0] is none_type else args[0]
return map_type(value_type).as_optional()
if str in args:
return map_type(str)
if args in {(int, float), (float, int)}:
return Number
return None


@cached
def map_type(cls, *, name: str = "", item_cls: Optional[type] = None):
"""Infer the converter type from a dataclass, type, or annotation."""
Expand All @@ -59,11 +74,10 @@ def map_type(cls, *, name: str = "", item_cls: Optional[type] = None):
return converter

if hasattr(types, "UnionType") and isinstance(cls, types.UnionType): # type: ignore
# Python 3.10 behavior
converter = map_type(cls.__args__[0])
assert len(cls.__args__) == 2
assert cls.__args__[1] == type(None)
converter = converter.as_optional()
args = tuple(getattr(cls, "__args__", ()))
converter = _convert_union(args)
if converter is None:
raise TypeError(f"Unsupported union type: {cls}")
return converter

if hasattr(cls, "__origin__"):
Expand Down Expand Up @@ -114,16 +128,16 @@ def map_type(cls, *, name: str = "", item_cls: Optional[type] = None):
converter = Dictionary.of_mapping(key, value)

elif cls.__origin__ == Union:
if str in cls.__args__:
converter = map_type(str)
if type(None) in cls.__args__:
converter = converter.as_optional()
elif cls.__args__ in {(int, float), (float, int)}:
converter = Number
args = tuple(cls.__args__)
if len(args) == 2:
converter = _convert_union(args)
else:
assert len(cls.__args__) == 2
assert cls.__args__[1] == type(None)
converter = map_type(cls.__args__[0]).as_optional()
if str in cls.__args__:
converter = map_type(str)
if type(None) in cls.__args__:
converter = converter.as_optional()
if converter is None:
raise TypeError(f"Unsupported union type: {cls}")

elif issubclass(cls.__origin__, Converter):
subtypes = [map_type(t) for t in cls.__args__]
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4.0"
markdown==3.9 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-get-deps==0.2.0 ; python_version >= "3.10" and python_version < "4.0"
mkdocs==1.6.1 ; python_version >= "3.10" and python_version < "4.0"
Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]

name = "datafiles"
version = "2.4"
version = "2.5"
description = "File-based ORM for dataclasses."

license = "MIT"
Expand Down Expand Up @@ -36,6 +36,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python",
"Topic :: Software Development",
"Topic :: Utilities",
Expand Down