Skip to content

Commit e537d72

Browse files
authored
🎨 New development decorators (#98)
Signed-off-by: Lukas Heumos <[email protected]>
1 parent f889c4b commit e537d72

8 files changed

+156
-28
lines changed

‎.pre-commit-config.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ fail_fast: false
22
default_language_version:
33
python: python3
44
default_stages:
5-
- commit
6-
- push
5+
- pre-commit
6+
- pre-push
77
minimum_pre_commit_version: 2.16.0
88
repos:
99
- repo: https://github.com/pre-commit/mirrors-prettier
@@ -24,7 +24,7 @@ repos:
2424
docs/notes/
2525
)
2626
- repo: https://github.com/astral-sh/ruff-pre-commit
27-
rev: v0.5.5
27+
rev: v0.9.1
2828
hooks:
2929
- id: ruff
3030
args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
@@ -44,7 +44,7 @@ repos:
4444
- id: trailing-whitespace
4545
- id: check-case-conflict
4646
- repo: https://github.com/pre-commit/mirrors-mypy
47-
rev: v1.7.1
47+
rev: v1.14.1
4848
hooks:
4949
- id: mypy
5050
args: [--no-strict-optional, --ignore-missing-imports]

‎lamin_utils/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
__version__ = "0.13.10"
44

5-
from ._core import colors
5+
try:
6+
from ._colors import colors
7+
except ImportError: # Backward compatibility
8+
from ._core import colors # type: ignore
69
from ._logger import logger
710
from ._python_version import py_version_warning
File renamed without changes.

‎lamin_utils/_compat.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
# BSD 3-Clause License
4+
# Copyright (c) 2017-2018 P. Angerer, F. Alexander Wolf, Theis Lab
5+
# All rights reserved.
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
# * Redistributions in binary form must reproduce the above copyright notice,
11+
# this list of conditions and the following disclaimer in the documentation
12+
# and/or other materials provided with the distribution.
13+
# * Neither the name of the copyright holder nor the names of its
14+
# contributors may be used to endorse or promote products derived from
15+
# this software without specific prior written permission.
16+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
import warnings
27+
from functools import wraps
28+
29+
30+
def deprecated(new_name=None, remove_in_version=None):
31+
"""Decorator to mark functions as deprecated and hide them from docs.
32+
33+
This is a decorator which can be used to mark functions, methods and properties as deprecated.
34+
It will result in a warning being emitted when the function is used.
35+
It will also hide the function from the docs.
36+
37+
Args:
38+
new_name: Name of the new function to use instead.
39+
If `None`, omits the new name notice.
40+
remove_in_version: Version when this will be removed.
41+
If `None`, omits the remove in version X notice.
42+
43+
Example:
44+
@property
45+
@deprecated("n_files")
46+
def n_objects(self) -> int:
47+
return self.n_files
48+
"""
49+
50+
def decorator(func):
51+
@wraps(func)
52+
def wrapper(*args, **kwargs):
53+
base_msg = f"{func.__name__} is deprecated"
54+
version_msg = (
55+
f" and will be removed in version {remove_in_version}"
56+
if remove_in_version
57+
else ""
58+
)
59+
migration_msg = f". Use {new_name} instead" if new_name else ""
60+
msg = f"{base_msg}{version_msg}{migration_msg}."
61+
warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
62+
return func(*args, **kwargs)
63+
64+
setattr(wrapper, "__deprecated", True)
65+
return wrapper
66+
67+
return decorator
68+
69+
70+
def future_change(change_version=None, change_description=None):
71+
"""Warns about future behavior changes.
72+
73+
Args:
74+
change_version: Version when behavior will change.
75+
change_description: Description of the behavior change.
76+
"""
77+
78+
def decorator(func):
79+
@wraps(func)
80+
def wrapper(*args, **kwargs):
81+
base_msg = f"{func.__name__} behavior will change"
82+
version_msg = f" in version {change_version}" if change_version else ""
83+
desc_msg = f". {change_description}" if change_description else ""
84+
msg = f"{base_msg}{version_msg}{desc_msg}."
85+
warnings.warn(msg, category=FutureWarning, stacklevel=2)
86+
return func(*args, **kwargs)
87+
88+
setattr(wrapper, "__future_change", True)
89+
return wrapper
90+
91+
return decorator
92+
93+
94+
def experimental(stable_version=None):
95+
"""Marks function as experimental/unstable API.
96+
97+
Args:
98+
stable_version: Expected version for API stabilization.
99+
"""
100+
101+
def decorator(func):
102+
@wraps(func)
103+
def wrapper(*args, **kwargs):
104+
base_msg = f"{func.__name__} is experimental"
105+
version_msg = f" until version {stable_version}" if stable_version else ""
106+
msg = f"{base_msg}{version_msg}. API may change without warning."
107+
warnings.warn(msg, category=UserWarning, stacklevel=2)
108+
return func(*args, **kwargs)
109+
110+
setattr(wrapper, "__experimental", True)
111+
return wrapper
112+
113+
return decorator

‎lamin_utils/_inspect.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Iterable
3+
from typing import TYPE_CHECKING
44

5-
from ._core import colors
5+
from ._colors import colors
66
from ._logger import logger
77
from ._map_synonyms import map_synonyms, to_str
88

99
if TYPE_CHECKING:
10+
from collections.abc import Iterable
11+
1012
import numpy as np
1113
import pandas as pd
1214

@@ -262,7 +264,7 @@ def _validate_logging(result: InspectResult, field: str | None = None) -> None:
262264
if len(result.non_validated) > 10:
263265
print_values += ", ..."
264266
warn_msg = (
265-
f"{colors.yellow(f'{len(result.non_validated)} unique term{s}')} ({(100-result.frac_validated):.2f}%)"
267+
f"{colors.yellow(f'{len(result.non_validated)} unique term{s}')} ({(100 - result.frac_validated):.2f}%)"
266268
f" {are} not validated{field_msg}: {colors.yellow(print_values)}"
267269
)
268270
if len(empty_warn_msg) > 0:

‎lamin_utils/_lookup.py

+23-18
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
from __future__ import annotations
2+
13
import re
24
from collections import namedtuple
3-
from typing import Any, Dict, Iterable, List, Optional, Tuple
5+
from typing import TYPE_CHECKING, Any
46

57
from ._logger import logger
68

9+
if TYPE_CHECKING:
10+
from collections.abc import Iterable
11+
712

8-
def _append_records_to_list(df_dict: Dict, value: str, record) -> None:
13+
def _append_records_to_list(df_dict: dict, value: str, record) -> None:
914
"""Append unique records to a list."""
1015
values_list = df_dict[value]
1116
if not isinstance(values_list, list):
@@ -20,19 +25,19 @@ def _append_records_to_list(df_dict: Dict, value: str, record) -> None:
2025

2126
def _create_df_dict(
2227
df: Any = None,
23-
field: Optional[str] = None,
24-
records: Optional[List] = None,
25-
values: Optional[List] = None,
26-
tuple_name: Optional[str] = None,
27-
) -> Dict:
28+
field: str | None = None,
29+
records: list | None = None,
30+
values: list | None = None,
31+
tuple_name: str | None = None,
32+
) -> dict:
2833
"""Create a dict with {lookup key: records in namedtuple}.
2934
3035
Value is a list of namedtuples if multiple records match the same key.
3136
"""
3237
if df is not None:
3338
records = df.itertuples(index=False, name=tuple_name)
3439
values = df[field]
35-
df_dict: Dict = {} # a dict of namedtuples as records and values as keys
40+
df_dict: dict = {} # a dict of namedtuples as records and values as keys
3641
for i, row in enumerate(records): # type:ignore
3742
value = values[i] # type:ignore
3843
if not isinstance(value, str):
@@ -52,12 +57,12 @@ class Lookup:
5257
# removed DataFrame type annotation to speed up import time
5358
def __init__(
5459
self,
55-
field: Optional[str] = None,
60+
field: str | None = None,
5661
tuple_name="MyTuple",
5762
prefix: str = "bt",
5863
df: Any = None,
59-
values: Optional[Iterable] = None,
60-
records: Optional[List] = None,
64+
values: Iterable | None = None,
65+
records: list | None = None,
6166
) -> None:
6267
self._tuple_name = tuple_name
6368
if df is not None:
@@ -78,13 +83,13 @@ def __init__(
7883
self._lookup_dict = self._create_lookup_dict(lkeys=lkeys, df_dict=self._df_dict)
7984
self._prefix = prefix
8085

81-
def _to_lookup_keys(self, values: Iterable, prefix: str) -> Dict:
86+
def _to_lookup_keys(self, values: Iterable, prefix: str) -> dict:
8287
"""Convert a list of strings to tab-completion allowed formats.
8388
8489
Returns:
8590
{lookup_key: value_or_values}
8691
"""
87-
lkeys: Dict = {}
92+
lkeys: dict = {}
8893
for value in list(values):
8994
if not isinstance(value, str):
9095
continue
@@ -103,8 +108,8 @@ def _to_lookup_keys(self, values: Iterable, prefix: str) -> Dict:
103108
lkeys[lkey] = value
104109
return lkeys
105110

106-
def _create_lookup_dict(self, lkeys: Dict, df_dict: Dict) -> Dict:
107-
lkey_dict: Dict = {} # a dict of namedtuples as records and lookup keys as keys
111+
def _create_lookup_dict(self, lkeys: dict, df_dict: dict) -> dict:
112+
lkey_dict: dict = {} # a dict of namedtuples as records and lookup keys as keys
108113
for lkey, values in lkeys.items():
109114
if isinstance(values, list):
110115
combined_list = []
@@ -120,18 +125,18 @@ def _create_lookup_dict(self, lkeys: Dict, df_dict: Dict) -> Dict:
120125

121126
return lkey_dict
122127

123-
def dict(self) -> Dict:
128+
def dict(self) -> dict:
124129
"""Dictionary of the lookup."""
125130
return self._df_dict
126131

127-
def lookup(self, return_field: Optional[str] = None) -> Tuple:
132+
def lookup(self, return_field: str | None = None) -> tuple:
128133
"""Lookup records with dot access."""
129134
# Names are invalid if they are conflict with Python keywords.
130135
if "class" in self._lookup_dict:
131136
self._lookup_dict[f"{self._prefix.lower()}_class"] = self._lookup_dict.pop(
132137
"class"
133138
)
134-
keys: List = list(self._lookup_dict.keys()) + ["dict"]
139+
keys: list = list(self._lookup_dict.keys()) + ["dict"]
135140
MyTuple = namedtuple("Lookup", keys) # type:ignore
136141
if return_field is not None:
137142
self._lookup_dict = {

‎lamin_utils/_map_synonyms.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Iterable, Literal
3+
from typing import TYPE_CHECKING, Literal
44

55
from ._logger import logger
66

77
if TYPE_CHECKING:
8+
from collections.abc import Iterable
9+
810
import pandas as pd
911

1012

‎lamin_utils/_standardize.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

33
from itertools import chain
4-
from typing import Any, Dict, Iterable, List, Literal, Union
4+
from typing import TYPE_CHECKING, Any, Literal
55

66
from ._logger import logger
77
from ._map_synonyms import map_synonyms
88

9+
if TYPE_CHECKING:
10+
from collections.abc import Iterable
11+
912

1013
def standardize(
1114
df: Any,

0 commit comments

Comments
 (0)