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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ repos:
hooks:
- id: pyupgrade
args:
- --py37-plus
- --py38-plus

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.8
Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "relattrs"
version = "1.0.0"
version = "1.1.0"
description = "Toolkit for working with nth order relational attributes in Python classes."
readme = {file = "README.rst", content-type = "text/x-rst"}
license = {text = "MIT"}
Expand Down Expand Up @@ -47,10 +47,13 @@ Homepage = "https://github.com/nibblex/python-relattrs"
[tool.setuptools]
include-package-data = true

[tool.setuptools.package-data]
relattrs = ["py.typed"]

[tool.pytest.ini_options]
python_files = ["tests.py", "test_*.py", "*_tests.py"]
pythonpath = [".", "relattrs"]
filterwarnings = ["ignore::DeprecationWarning"]
pythonpath = ["."]
addopts = "--doctest-modules"

[tool.tox]
envlist = ["py38", "py39", "py310", "py311", "py312", "py313", "py314"]
Expand All @@ -66,4 +69,4 @@ line-length = 88
indent-width = 4

[tool.ruff.lint]
select = ["I"]
select = ["E", "F", "W", "I", "UP", "B"]
52 changes: 30 additions & 22 deletions relattrs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
from functools import reduce
from typing import Any, Optional

__all__ = ["rgetattr", "rhasattr", "rsetattr", "rdelattr"]


def rgetattr(obj: object, rattr: str, /, *default, sep: Optional[str] = None) -> Any:
"""
Recursively gets an attribute from an object based on a dotted string representation.
Recursively gets an attribute from an object using a dotted string path.

Args:
obj: The object from which to retrieve the attribute.
rattr: The dotted string representation of the attribute to retrieve.
default: The default value to return if the attribute does not exist.
sep: The separator used to split the string representation. Defaults to '.' (dot).
sep: The separator used to split the string representation. Defaults to '.'.

Returns:
Any: The value of the attribute.

Raises:
AttributeError: If the attribute does not exist and no default value is provided.
AttributeError: If the attribute does not exist and no default is provided.
TypeError: If more than one default value is provided.

Example:
>>> from relattrs import rgetattr
Expand All @@ -29,24 +32,29 @@ def rgetattr(obj: object, rattr: str, /, *default, sep: Optional[str] = None) ->
1
"""

rattr = rattr.split(sep or ".")
if len(default) == 1:
if len(default) > 1:
raise TypeError(
f"rgetattr expected at most 1 default value, got {len(default)}"
)

parts = rattr.split(sep or ".")
if default:
try:
return reduce(getattr, rattr, obj)
return reduce(getattr, parts, obj)
except AttributeError:
return default[0]

return reduce(getattr, rattr, obj)
return reduce(getattr, parts, obj)


def rhasattr(obj: object, rattr: str, /, *, sep: Optional[str] = None) -> bool:
"""
Recursively checks if an object has an attribute based on a dotted string representation.
Recursively checks if an object has an attribute using a dotted string path.

Args:
obj: The object to check.
rattr: The dotted string representation of the attribute to check.
sep: The separator used to split the string representation. Defaults to '.' (dot).
sep: The separator used to split the string representation. Defaults to '.'.

Returns:
bool: True if the attribute exists, False otherwise.
Expand All @@ -64,10 +72,10 @@ def rhasattr(obj: object, rattr: str, /, *, sep: Optional[str] = None) -> bool:
False
"""

rattr = rattr.split(sep or ".")
parts = rattr.split(sep or ".")
try:
obj = reduce(getattr, rattr[:-1], obj)
return hasattr(obj, rattr[-1])
obj = reduce(getattr, parts[:-1], obj)
return hasattr(obj, parts[-1])
except AttributeError:
return False

Expand All @@ -82,7 +90,7 @@ def rsetattr(
obj: The object on which to set the attribute.
rattr: The dotted string representation of the attribute to set.
val: The value to set.
sep: The separator used to split the string representation. Defaults to '.' (dot).
sep: The separator used to split the string representation. Defaults to '.'.

Example:
>>> from relattrs import rsetattr
Expand All @@ -96,22 +104,22 @@ def rsetattr(
2
"""

rattr = rattr.split(sep or ".")
obj = reduce(getattr, rattr[:-1], obj)
setattr(obj, rattr[-1], val)
parts = rattr.split(sep or ".")
obj = reduce(getattr, parts[:-1], obj)
setattr(obj, parts[-1], val)


def rdelattr(obj: object, rattr: str, /, *, sep: Optional[str] = None) -> None:
"""
Recursively deletes an attribute from an object based on a dotted string representation.
Recursively deletes an attribute from an object using a dotted string path.

Args:
obj: The object from which to delete the attribute.
rattr: The dotted string representation of the attribute to delete.
sep: The separator used to split the string representation. Defaults to '.' (dot).
sep: The separator used to split the string representation. Defaults to '.'.

Example:
>>> from relattrs import rdelattr
>>> from relattrs import rdelattr, rhasattr
>>> class A:
... class B:
... class C:
Expand All @@ -122,6 +130,6 @@ def rdelattr(obj: object, rattr: str, /, *, sep: Optional[str] = None) -> None:
False
"""

rattr = rattr.split(sep or ".")
obj = reduce(getattr, rattr[:-1], obj)
delattr(obj, rattr[-1])
parts = rattr.split(sep or ".")
obj = reduce(getattr, parts[:-1], obj)
delattr(obj, parts[-1])
Empty file added relattrs/py.typed
Empty file.
5 changes: 5 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def test_rgetattr(container, sep, attr_path, expected):
assert rgetattr(container, attr_path, sep=sep) == expected


def test_rgetattr_multiple_defaults_raises(container):
with pytest.raises(TypeError):
rgetattr(container, "non_existent", "default1", "default2")


@pytest.mark.parametrize("sep", separators)
@pytest.mark.parametrize(
("attr_path", "expected"),
Expand Down
Loading