Skip to content

Commit

Permalink
feat: 2nd commit (#2)
Browse files Browse the repository at this point in the history
* feat: 2nd commit
* build: add typing_extensions
* test: exclude pypy

Signed-off-by: nstarman <nstarman@users.noreply.github.com>
  • Loading branch information
nstarman authored Jul 17, 2024
1 parent 638875b commit d8e0545
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 73 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.12"]
python-version: ["3.10", "3.12"]
runs-on: [ubuntu-latest, macos-latest, windows-latest]

include:
- python-version: pypy-3.10
runs-on: ubuntu-latest
# include:
# - python-version: pypy-3.10
# runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import annotations
"""Sphinx configuration."""

import importlib.metadata

project = "dataclasstools"
copyright = "2024, Nathaniel Starkman"
copyright = "2024, Nathaniel Starkman" # noqa: A001
author = "Nathaniel Starkman"
version = release = importlib.metadata.version("dataclasstools")

Expand Down
47 changes: 22 additions & 25 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import annotations
"""Nox configuration."""

import argparse
import shutil
Expand All @@ -15,20 +15,20 @@

@nox.session
def lint(session: nox.Session) -> None:
"""
Run the linter.
"""
"""Run the linter."""
session.install("pre-commit")
session.run(
"pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs
"pre-commit",
"run",
"--all-files",
"--show-diff-on-failure",
*session.posargs,
)


@nox.session
def pylint(session: nox.Session) -> None:
"""
Run PyLint.
"""
"""Run PyLint."""
# This needs to be installed into the package environment, and is slower
# than a pre-commit check
session.install(".", "pylint")
Expand All @@ -37,23 +37,21 @@ def pylint(session: nox.Session) -> None:

@nox.session
def tests(session: nox.Session) -> None:
"""
Run the unit and regular tests.
"""
"""Run the unit and regular tests."""
session.install(".[test]")
session.run("pytest", *session.posargs)


@nox.session(reuse_venv=True)
def docs(session: nox.Session) -> None:
"""
Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links.
"""

"""Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links."""
parser = argparse.ArgumentParser()
parser.add_argument("--serve", action="store_true", help="Serve after building")
parser.add_argument(
"-b", dest="builder", default="html", help="Build target (default: html)"
"-b",
dest="builder",
default="html",
help="Build target (default: html)",
)
args, posargs = parser.parse_known_args(session.posargs)

Expand All @@ -67,7 +65,12 @@ def docs(session: nox.Session) -> None:

if args.builder == "linkcheck":
session.run(
"sphinx-build", "-b", "linkcheck", ".", "_build/linkcheck", *posargs
"sphinx-build",
"-b",
"linkcheck",
".",
"_build/linkcheck",
*posargs,
)
return

Expand All @@ -88,10 +91,7 @@ def docs(session: nox.Session) -> None:

@nox.session
def build_api_docs(session: nox.Session) -> None:
"""
Build (regenerate) API docs.
"""

"""Build (regenerate) API docs."""
session.install("sphinx")
session.chdir("docs")
session.run(
Expand All @@ -107,10 +107,7 @@ def build_api_docs(session: nox.Session) -> None:

@nox.session
def build(session: nox.Session) -> None:
"""
Build an SDist and wheel.
"""

"""Build an SDist and wheel."""
build_path = DIR.joinpath("build")
if build_path.exists():
shutil.rmtree(build_path)
Expand Down
52 changes: 18 additions & 34 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors = [
description = "dataclass tools, extended by multiple dispatch"
readme = "README.md"
license.file = "LICENSE"
requires-python = ">=3.8"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 1 - Planning",
"Intended Audience :: Science/Research",
Expand All @@ -21,16 +21,17 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering",
"Typing :: Typed",
]
dynamic = ["version"]
dependencies = []
dependencies = [
"plum-dispatch>=2.5.1",
"typing_extensions",
]

[project.optional-dependencies]
test = [
Expand Down Expand Up @@ -86,8 +87,8 @@ report.exclude_also = [
]

[tool.mypy]
files = ["src", "tests"]
python_version = "3.8"
files = ["src"]
python_version = "3.10"
warn_unused_configs = true
strict = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
Expand All @@ -100,50 +101,33 @@ module = "dataclasstools.*"
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
ignore_missing_imports = true
module = ["plum.*"]


[tool.ruff]
src = ["src"]

[tool.ruff.lint]
extend-select = [
"B", # flake8-bugbear
"I", # isort
"ARG", # flake8-unused-arguments
"C4", # flake8-comprehensions
"EM", # flake8-errmsg
"ICN", # flake8-import-conventions
"G", # flake8-logging-format
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
"RET", # flake8-return
"RUF", # Ruff-specific
"SIM", # flake8-simplify
"T20", # flake8-print
"UP", # pyupgrade
"YTT", # flake8-2020
"EXE", # flake8-executable
"NPY", # NumPy specific rules
"PD", # pandas-vet
]
extend-select = ["ALL"]
ignore = [
"ANN401", # Dynamically typed expressions are disallowed in `**kwargs`
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the first line
"PLR09", # Too many <...>
"PLR2004", # Magic value used in comparison
"ISC001", # Conflicts with formatter
]
isort.required-imports = ["from __future__ import annotations"]
# Uncomment if using a _compat.typing backport
# typing-modules = ["dataclasstools._compat.typing"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]
"tests/**" = ["ANN", "S101", "T20"]
"noxfile.py" = ["T20"]
"docs/conf.py" = ["INP001"]


[tool.pylint]
py-version = "3.8"
py-version = "3.10"
ignore-paths = [".*/_version.py"]
reports.output-format = "colorized"
similarities.ignore-imports = "yes"
Expand Down
20 changes: 15 additions & 5 deletions src/dataclasstools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
"""
Copyright (c) 2024 Nathaniel Starkman. All rights reserved.
"""Copyright (c) 2024 Nathaniel Starkman. All rights reserved.
dataclasstools: dataclass tools, extended by multiple dispatch
"""

from __future__ import annotations

from ._core import DataclassInstance, asdict, astuple, fields, replace
from ._ext import field_items, field_values
from ._version import version as __version__

__all__ = ["__version__"]
__all__ = [
"__version__",
# core
"DataclassInstance",
"replace",
"fields",
"asdict",
"astuple",
# ext
"field_values",
"field_items",
]
60 changes: 60 additions & 0 deletions src/dataclasstools/_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Core module for ``dataclasstools``."""

__all__ = ["DataclassInstance", "replace", "fields", "asdict", "astuple"]

from collections.abc import Callable
from dataclasses import Field as _dataclass_Field
from dataclasses import asdict as _dataclass_asdict
from dataclasses import astuple as _dataclass_astuple
from dataclasses import fields as _dataclass_fields
from dataclasses import replace as _dataclass_replace
from typing import Any, ClassVar, Protocol, runtime_checkable

from plum import dispatch


@runtime_checkable
class DataclassInstance(Protocol):
"""Protocol for dataclass instances."""

__dataclass_fields__: ClassVar[dict[str, Any]]

# B/c of https://github.com/python/mypy/issues/3939 just having
# `__dataclass_fields__` is insufficient for `issubclass` checks.
@classmethod
def __subclasshook__(cls: type, c: type) -> bool:
"""Customize the subclass check."""
return hasattr(c, "__dataclass_fields__")


@dispatch # type: ignore[misc]
def replace(obj: DataclassInstance, /, **kwargs: Any) -> DataclassInstance:
"""Replace the fields of a dataclass instance."""
return _dataclass_replace(obj, **kwargs)


@dispatch # type: ignore[misc]
def fields(obj: DataclassInstance) -> tuple[_dataclass_Field[Any], ...]:
"""Return the fields of a dataclass instance."""
return _dataclass_fields(obj)


@dispatch # type: ignore[misc]
def asdict(
obj: DataclassInstance,
/,
*,
dict_factory: Callable[[list[tuple[str, Any]]], dict[str, Any]] = dict,
) -> dict[str, Any]:
"""Return the fields of a dataclass instance as a dictionary."""
return _dataclass_asdict(obj, dict_factory=dict_factory)


@dispatch # type: ignore[misc]
def astuple(
obj: DataclassInstance,
/,
tuple_factory: Callable[[Any], tuple[Any, ...]] = tuple,
) -> tuple[Any, ...]:
"""Return the fields of a dataclass instance as a tuple."""
return _dataclass_astuple(obj, tuple_factory=tuple_factory)
22 changes: 22 additions & 0 deletions src/dataclasstools/_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Extension functions for ``dataclasstools``."""

__all__ = ["field_values", "field_items"]

from collections.abc import Iterator
from typing import Any

from plum import dispatch

from ._core import DataclassInstance, fields


@dispatch # type: ignore[misc]
def field_values(obj: DataclassInstance) -> Iterator[Any]:
"""Return the values of a dataclass instance."""
yield from (getattr(obj, f.name) for f in fields(obj))


@dispatch # type: ignore[misc]
def field_items(obj: DataclassInstance) -> Iterator[tuple[str, Any]]:
"""Return the field names and values of a dataclass instance."""
yield from ((f.name, getattr(obj, f.name)) for f in fields(obj))
2 changes: 0 additions & 2 deletions src/dataclasstools/_version.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
from __future__ import annotations

version: str
version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests."""
3 changes: 2 additions & 1 deletion tests/test_package.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations
"""Test the package metadata."""

import importlib.metadata

import dataclasstools as m


def test_version():
"""Test that the package version matches the metadata."""
assert importlib.metadata.version("dataclasstools") == m.__version__

0 comments on commit d8e0545

Please sign in to comment.